// ----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ----------------------------------------------------------------------- namespace Microsoft.Samples.Kinect.Webserver { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using System.Web.Script.Serialization; using Microsoft.Samples.Kinect.Webserver.Properties; /// /// Static class that defines extensions used to serialize/deserialize objects to/from /// JSON strings. /// public static class JsonSerializationExtensions { /// /// Serialization default buffer size. /// private const int BufferSize = 512; /// /// Serialize specified object to a stream as UTF8-encoded JSON. /// /// /// Type of object to serialize. /// /// /// Object to serialize. /// /// /// Stream where UTF8-encoded JSON representing object will be output. /// public static void ToJson(this T obj, Stream outputStream) { using (var writer = new StreamWriter(outputStream, new UTF8Encoding(false), BufferSize, true)) { writer.Write(obj.ToJson()); } } /// /// Serialize specified object to a JSON string. /// /// /// Type of object to serialize. /// /// /// Object to serialize. /// /// /// JSON string representing serialized object. /// public static string ToJson(this T obj) { return (new JavaScriptSerializer()).Serialize(obj); } /// /// Serialize specified dictionary to a stream as UTF8-encoded JSON. /// /// /// Dictionary to serialize. /// /// /// Stream where UTF8-encoded JSON representing dictionary will be output. /// /// /// /// Only IDictionary<string,object> objects and default types will be /// recognized by the serializer. /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// will be serialized as {"foo":23,"bar":"tar"}, rather than as /// [{"Key":"foo","Value":23},{"Key":"bar","Value":"tar"}], which is the default /// way to serialize dictionary objects as JSON. /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// public static void DictionaryToJson(this IDictionary dictionary, Stream outputStream) { // Leave output stream open after we're done writing using (var writer = new StreamWriter(outputStream, new UTF8Encoding(false), BufferSize, true)) { writer.Write(dictionary.DictionaryToJson()); } } /// /// Asynchronously serialize specified dictionary to a stream as UTF8-encoded JSON. /// /// /// Dictionary to serialize. /// /// /// Stream where UTF8-encoded JSON representing dictionary will be output. /// /// /// Await-able task. /// /// /// /// Only IDictionary<string,object> objects and default types will be /// recognized by the serializer. /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// will be serialized as {"foo":23,"bar":"tar"}, rather than as /// [{"Key":"foo","Value":23},{"Key":"bar","Value":"tar"}], which is the default /// way to serialize dictionary objects as JSON. /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// public static async Task DictionaryToJsonAsync(this IDictionary dictionary, Stream outputStream) { // Leave output stream open after we're done writing using (var writer = new StreamWriter(outputStream, new UTF8Encoding(false), BufferSize, true)) { await writer.WriteAsync(dictionary.DictionaryToJson()); } } /// /// Serialize specified dictionary to a JSON string. /// /// /// Dictionary to serialize. /// /// /// JSON string representing serialized object. /// /// /// /// Only IDictionary<string,object> objects and default types will be /// recognized by the serializer. /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// will be serialized as {"foo":23,"bar":"tar"}, rather than as /// [{"Key":"foo","Value":23},{"Key":"bar","Value":"tar"}], which is the default /// way to serialize dictionary objects as JSON. /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// public static string DictionaryToJson(this IDictionary dictionary) { return (new JavaScriptSerializer()).Serialize(dictionary); } /// /// Deserialize specified object from UTF8-encoded JSON read from a stream. /// /// /// Type of object to deserialize. /// /// /// Stream from which to read UTF8-encoded JSON representing serialized object. /// /// /// Deserialized object corresponding to input JSON. /// public static T FromJson(this Stream inputStream) { using (var reader = new StreamReader(inputStream, Encoding.UTF8)) { return reader.ReadToEnd().FromJson(); } } /// /// Deserialize specified object from a JSON string. /// /// /// Type of object to deserialize. /// /// /// JSON string representing serialized object. /// /// /// Deserialized object corresponding to JSON string. /// /// /// Errors encountered during serialization might throw SerializationException. /// public static T FromJson(this string input) { try { return (new JavaScriptSerializer()).Deserialize(input); // Convert exceptions to Serialization exception to provide a single exception to // catch for callers. } catch (ArgumentException e) { throw new SerializationException(@"Exception encountered deserializing JSON string", e); } catch (InvalidOperationException e) { throw new SerializationException(@"Exception encountered deserializing JSON string", e); } } /// /// Deserialize specified dictionary from a stream as UTF8-encoded JSON. /// /// /// Stream containing UTF8-encoded JSON representation of dictionary. /// /// /// Deserialized dictionary. /// /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a JSON object such as /// {"foo":23,"bar":"tar"} will be deserialized as a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// public static Dictionary DictionaryFromJson(this Stream inputStream) { using (var reader = new StreamReader(inputStream, Encoding.UTF8)) { return reader.ReadToEnd().DictionaryFromJson(); } } /// /// Asynchronously deserialize specified dictionary from a stream as UTF8-encoded JSON. /// /// /// Stream containing UTF8-encoded JSON representation of dictionary. /// /// /// Await-able task Deserialized dictionary. /// /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a JSON object such as /// {"foo":23,"bar":"tar"} will be deserialized as a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Clients won't have to create nested structure themselves. They will just await on task to get dictionary object.")] public static async Task> DictionaryFromJsonAsync(this Stream inputStream) { using (var reader = new StreamReader(inputStream, Encoding.UTF8)) { var input = await reader.ReadToEndAsync(); return input.DictionaryFromJson(); } } /// /// Deserialize specified dictionary from a JSON string. /// /// /// JSON string containing JSON representation of dictionary. /// /// /// Deserialized dictionary. /// /// /// /// Dictionaries mapping string keys to object values will be treated as direct /// representations of JSON objects, so that a JSON object such as /// {"foo":23,"bar":"tar"} will be deserialized as a dictionary that contains: /// /// /// a key "foo" that maps to a numeric value of 23 /// /// /// a key "bar" that maps to a string value of "tar" /// /// /// /// /// This method does not look for circular references and therefore does not /// support them. /// /// /// Errors encountered during serialization might throw SerializationException. /// /// public static Dictionary DictionaryFromJson(this string input) { try { return (new JavaScriptSerializer()).Deserialize>(input); // Convert exceptions to Serialization exception to provide a single exception to // catch for callers. } catch (ArgumentException e) { throw new SerializationException(@"Exception encountered deserializing JSON string", e); } catch (InvalidOperationException e) { throw new SerializationException(@"Exception encountered deserializing JSON string", e); } } /// /// Extract serializable JSON data from specified value, ensuring that property names /// match JSON naming conventions (camel case). /// /// The object to be converted. /// The converted object. internal static object ExtractSerializableJsonData(object value) { if (value == null || IsSerializationPrimitive(value)) { return value; } if (IsDictionary(value)) { // The key type for the given dictionary must be string type. if (!IsSerializableGenericDictionary(value) && !IsSerializableDictionary(value)) { throw new NotSupportedException(Resources.UnsupportedKeyType); } var result = new Dictionary(); var dict = (IDictionary)value; foreach (var key in dict.Keys) { result.Add((string)key, ExtractSerializableJsonData(dict[key])); } return result; } if (IsEnumerable(value)) { // For the object with IEnumerable interface, serialize each items in it. var result = new List(); foreach (var v in (IEnumerable)value) { result.Add(ExtractSerializableJsonData(v)); } return result; } else { var result = new Dictionary(); foreach (var p in value.GetType().GetProperties()) { if (IsSerializableProperty(p)) { result.Add(ToCamelCase(p.Name), ExtractSerializableJsonData(p.GetValue(value))); } } return result; } } /// /// Filters the classes represented in an array of Type objects. /// /// The Type object to which the filter is applied. /// An arbitrary object used to filter the list. /// True to include the Type in the filtered list; otherwise false. private static bool InterfaceTypeFilter(Type type, object criteria) { var targetType = criteria as Type; if (type.IsGenericType) { return type.GetGenericTypeDefinition() == targetType; } return targetType != null && type == targetType; } /// /// Find the target interface from a given object. /// /// The given object. /// The target interface to be found. /// The interface which has been found. private static Type FindTargetInterface(object value, Type targetInterface) { var types = value.GetType().FindInterfaces(InterfaceTypeFilter, targetInterface); return (types.Length > 0) ? types[0] : null; } /// /// Determine if the given object has a serializable generic dictionary interface. /// /// The given object. /// Returns true if it has the interface; otherwise false. private static bool IsSerializableGenericDictionary(object value) { var genericDictType = typeof(IDictionary).GetGenericTypeDefinition(); var type = FindTargetInterface(value, genericDictType); if (type != null) { // Only the dictionaries with string type keys are serializable. var argumentTypes = type.GetGenericArguments(); return argumentTypes.Length > 0 && argumentTypes[0] == typeof(string); } return false; } /// /// Determine if the given object has a serializable dictionary interface. /// /// The given object. /// Returns true if it has the interface; otherwise false. private static bool IsSerializableDictionary(object value) { var type = FindTargetInterface(value, typeof(IDictionary)); if (type == null) { return false; } foreach (var key in ((IDictionary)value).Keys) { if (!(key is string)) { return false; } } return true; } /// /// Determine if the given object has a dictionary or generic dictionary interface. /// /// The given object. /// Returns true if it has the interface; otherwise false. private static bool IsDictionary(object value) { var dictType = FindTargetInterface(value, typeof(IDictionary)); if (dictType == null) { dictType = FindTargetInterface(value, typeof(IDictionary).GetGenericTypeDefinition()); } return dictType != null; } /// /// Determine if the given object has an IEnumerable interface. /// /// The given object. /// Returns true if it has the interface; otherwise false. private static bool IsEnumerable(object value) { var type = FindTargetInterface(value, typeof(IEnumerable)); return type != null; } /// /// Convert the given string to camelCase. /// /// The given string. /// The returned string. private static string ToCamelCase(string text) { if (string.IsNullOrEmpty(text)) { return text; } return string.Format(CultureInfo.CurrentCulture, "{0}{1}", char.ToLower(text[0], CultureInfo.CurrentCulture), text.Substring(1)); } /// /// Determine if the given object is a primitive to be serialized. /// /// The input object. /// Return true if it is a serialization primitive, otherwise return false. private static bool IsSerializationPrimitive(object o) { Type t = o.GetType(); return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(DateTime); } /// /// Determine if the given property is serializable. /// /// The input property. /// Return true if it is serializable, otherwise return false. private static bool IsSerializableProperty(PropertyInfo propertyInfo) { foreach (var accessor in propertyInfo.GetAccessors()) { if (accessor.IsPublic && !accessor.IsStatic) { return propertyInfo.GetGetMethod(false) != null && propertyInfo.CanWrite; } } return false; } } }