using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Text; using System.Xml; using System.Xml.Serialization; using JetBrains.Annotations; namespace Microsoft.Win32.TaskScheduler { internal static class XmlSerializationHelper { public static object GetDefaultValue([NotNull] PropertyInfo prop) { var attributes = prop.GetCustomAttributes(typeof(DefaultValueAttribute), true); if (attributes.Length > 0) { var defaultAttr = (DefaultValueAttribute)attributes[0]; return defaultAttr.Value; } // Attribute not found, fall back to default value for the type if (prop.PropertyType.IsValueType) return Activator.CreateInstance(prop.PropertyType); return null; } private static bool GetPropertyValue(object obj, [NotNull] string property, ref object outVal) { PropertyInfo pi = obj?.GetType().GetProperty(property); if (pi != null) { outVal = pi.GetValue(obj, null); return true; } return false; } private static bool GetAttributeValue(Type objType, Type attrType, string property, bool inherit, ref object outVal) { object[] attrs = objType.GetCustomAttributes(attrType, inherit); if (attrs.Length > 0) return GetPropertyValue(attrs[0], property, ref outVal); return false; } private static bool GetAttributeValue([NotNull] PropertyInfo propInfo, Type attrType, string property, bool inherit, ref object outVal) { Attribute attr = Attribute.GetCustomAttribute(propInfo, attrType, inherit); return GetPropertyValue(attr, property, ref outVal); } private static bool IsStandardType(Type type) => type.IsPrimitive || type == typeof(DateTime) || type == typeof(DateTimeOffset) || type == typeof(Decimal) || type == typeof(Guid) || type == typeof(TimeSpan) || type == typeof(string) || type.IsEnum; private static bool HasMembers([NotNull] object obj) { if (obj is IXmlSerializable) { using (System.IO.MemoryStream mem = new System.IO.MemoryStream()) { using (XmlTextWriter tw = new XmlTextWriter(mem, Encoding.UTF8)) { ((IXmlSerializable)obj).WriteXml(tw); tw.Flush(); return mem.Length > 3; } } } // Enumerate each public property PropertyInfo[] props = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (var pi in props) { if (!Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false)) { object value = pi.GetValue(obj, null); if (!Equals(value, GetDefaultValue(pi))) { if (!IsStandardType(pi.PropertyType)) { if (HasMembers(value)) return true; } else return true; } } } return false; } public static string GetPropertyAttributeName([NotNull] PropertyInfo pi) { object oVal = null; string eName = pi.Name; if (GetAttributeValue(pi, typeof(XmlAttributeAttribute), "AttributeName", false, ref oVal)) eName = oVal.ToString(); return eName; } public static string GetPropertyElementName([NotNull] PropertyInfo pi) { object oVal = null; string eName = pi.Name; if (GetAttributeValue(pi, typeof(XmlElementAttribute), "ElementName", false, ref oVal)) eName = oVal.ToString(); else if (GetAttributeValue(pi.PropertyType, typeof(XmlRootAttribute), "ElementName", true, ref oVal)) eName = oVal.ToString(); return eName; } public delegate bool PropertyConversionHandler([NotNull] PropertyInfo pi, Object obj, ref Object value); public static bool WriteProperty([NotNull] XmlWriter writer, [NotNull] PropertyInfo pi, [NotNull] Object obj, PropertyConversionHandler handler = null) { if (Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false) || Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false)) return false; object value = pi.GetValue(obj, null); object defValue = GetDefaultValue(pi); if ((value == null && defValue == null) || (value != null && value.Equals(defValue))) return false; Type propType = pi.PropertyType; if (handler != null && handler(pi, obj, ref value)) propType = value.GetType(); bool isStdType = IsStandardType(propType); bool rw = pi.CanRead && pi.CanWrite; bool ro = pi.CanRead && !pi.CanWrite; string eName = GetPropertyElementName(pi); if (isStdType && rw) { string output = GetXmlValue(value, propType); if (output != null) writer.WriteElementString(eName, output); } else if (!isStdType) { object outVal = null; if (propType.GetInterface("IXmlSerializable") == null && GetAttributeValue(pi, typeof(XmlArrayAttribute), "ElementName", true, ref outVal) && propType.GetInterface("IEnumerable") != null) { if (string.IsNullOrEmpty(outVal.ToString())) outVal = eName; writer.WriteStartElement(outVal.ToString()); var attributes = Attribute.GetCustomAttributes(pi, typeof(XmlArrayItemAttribute), true); var dict = new Dictionary(attributes.Length); foreach (XmlArrayItemAttribute a in attributes) dict.Add(a.Type, a.ElementName); foreach (object item in ((System.Collections.IEnumerable)value)) { string aeName; Type itemType = item.GetType(); if (dict.TryGetValue(itemType, out aeName)) { if (IsStandardType(itemType)) writer.WriteElementString(aeName, GetXmlValue(item, itemType)); else WriteObject(writer, item, null, false, aeName); } } writer.WriteEndElement(); } else WriteObject(writer, value); } return false; } private static string GetXmlValue([NotNull] object value, Type propType) { string output = null; if (propType.IsEnum) { if (Attribute.IsDefined(propType, typeof(FlagsAttribute), false)) output = Convert.ChangeType(value, Enum.GetUnderlyingType(propType)).ToString(); else output = value.ToString(); } else { switch (propType.FullName) { case "System.Boolean": output = XmlConvert.ToString((System.Boolean)value); break; case "System.Byte": output = XmlConvert.ToString((System.Byte)value); break; case "System.Char": output = XmlConvert.ToString((System.Char)value); break; case "System.DateTime": output = XmlConvert.ToString((System.DateTime)value, XmlDateTimeSerializationMode.RoundtripKind); break; case "System.DateTimeOffset": output = XmlConvert.ToString((System.DateTimeOffset)value); break; case "System.Decimal": output = XmlConvert.ToString((System.Decimal)value); break; case "System.Double": output = XmlConvert.ToString((System.Double)value); break; case "System.Single": output = XmlConvert.ToString((System.Single)value); break; case "System.Guid": output = XmlConvert.ToString((System.Guid)value); break; case "System.Int16": output = XmlConvert.ToString((System.Int16)value); break; case "System.Int32": output = XmlConvert.ToString((System.Int32)value); break; case "System.Int64": output = XmlConvert.ToString((System.Int64)value); break; case "System.SByte": output = XmlConvert.ToString((System.SByte)value); break; case "System.TimeSpan": output = XmlConvert.ToString((System.TimeSpan)value); break; case "System.UInt16": output = XmlConvert.ToString((System.UInt16)value); break; case "System.UInt32": output = XmlConvert.ToString((System.UInt32)value); break; case "System.UInt64": output = XmlConvert.ToString((System.UInt64)value); break; default: output = value == null ? string.Empty : value.ToString(); break; } } return output; } public static void WriteObjectAttributes([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null) { // Enumerate each property foreach (var pi in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) if (Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false)) WriteObjectAttribute(writer, pi, obj, handler); } public static void WriteObjectAttribute([NotNull] XmlWriter writer, [NotNull] PropertyInfo pi, [NotNull] object obj, PropertyConversionHandler handler = null) { object value = pi.GetValue(obj, null); object defValue = GetDefaultValue(pi); if ((value == null && defValue == null) || (value != null && value.Equals(defValue))) return; Type propType = pi.PropertyType; if (handler != null && handler(pi, obj, ref value)) propType = value.GetType(); writer.WriteAttributeString(GetPropertyAttributeName(pi), GetXmlValue(value, propType)); } public static void WriteObjectProperties([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null) { // Enumerate each public property foreach (var pi in obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) WriteProperty(writer, pi, obj, handler); } public static void WriteObject([NotNull] XmlWriter writer, [NotNull] object obj, PropertyConversionHandler handler = null, bool includeNS = false, string elemName = null) { if (obj == null) return; // Get name of top level element string oName = elemName ?? GetElementName(obj); if (!HasMembers(obj)) return; if (includeNS) writer.WriteStartElement(oName, GetTopLevelNamespace(obj)); else writer.WriteStartElement(oName); if (obj is IXmlSerializable) { ((IXmlSerializable)obj).WriteXml(writer); } else { WriteObjectAttributes(writer, obj, handler); WriteObjectProperties(writer, obj, handler); } writer.WriteEndElement(); } public static string GetElementName([NotNull] object obj) { object oVal = null; return GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "ElementName", true, ref oVal) ? oVal.ToString() : obj.GetType().Name; } public static string GetTopLevelNamespace([NotNull] object obj) { object oVal = null; return GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "Namespace", true, ref oVal) ? oVal.ToString() : null; } public static void ReadObjectProperties([NotNull] XmlReader reader, [NotNull] object obj, PropertyConversionHandler handler = null) { // Build property lookup table PropertyInfo[] props = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); Dictionary attrHash = new Dictionary(props.Length); Dictionary propHash = new Dictionary(props.Length); foreach (var pi in props) { if (!Attribute.IsDefined(pi, typeof(XmlIgnoreAttribute), false)) { if (Attribute.IsDefined(pi, typeof(XmlAttributeAttribute), false)) attrHash.Add(GetPropertyAttributeName(pi), pi); else propHash.Add(GetPropertyElementName(pi), pi); } } if (reader.HasAttributes) { for (int i = 0; i < reader.AttributeCount; i++) { PropertyInfo pi; reader.MoveToAttribute(i); if (attrHash.TryGetValue(reader.LocalName, out pi)) { if (IsStandardType(pi.PropertyType)) { object value = null; if (pi.PropertyType.IsEnum) value = Enum.Parse(pi.PropertyType, reader.Value); else value = Convert.ChangeType(reader.Value, pi.PropertyType); if (handler != null) handler(pi, obj, ref value); pi.SetValue(obj, value, null); } } } } while (reader.MoveToContent() == XmlNodeType.Element) { PropertyInfo pi; object outVal = null; if (propHash.TryGetValue(reader.LocalName, out pi)) { var tc = TypeDescriptor.GetConverter(pi.PropertyType); if (IsStandardType(pi.PropertyType)) { object value = null; if (pi.PropertyType.IsEnum) value = Enum.Parse(pi.PropertyType, reader.ReadElementContentAsString()); else if (pi.PropertyType == typeof(Guid)) value = new GuidConverter().ConvertFromString(reader.ReadElementContentAsString()); else value = reader.ReadElementContentAs(pi.PropertyType, null); if (handler != null) handler(pi, obj, ref value); pi.SetValue(obj, value, null); } else if (pi.PropertyType == typeof(Version)) { Version v = new Version(reader.ReadElementContentAsString()); pi.SetValue(obj, v, null); } else if (pi.PropertyType.GetInterface("IEnumerable") != null && pi.PropertyType.GetInterface("IXmlSerializable") == null && GetAttributeValue(pi, typeof(XmlArrayAttribute), "ElementName", true, ref outVal)) { string elem = string.IsNullOrEmpty(outVal?.ToString()) ? pi.Name : outVal.ToString(); reader.ReadStartElement(elem); var attributes = Attribute.GetCustomAttributes(pi, typeof(XmlArrayItemAttribute), true); var dict = new Dictionary(attributes.Length); foreach (XmlArrayItemAttribute a in attributes) dict.Add(a.ElementName, a.Type); List output = new List(); while (reader.MoveToContent() == XmlNodeType.Element) { Type itemType; if (dict.TryGetValue(reader.LocalName, out itemType)) { object o; if (IsStandardType(itemType)) o = reader.ReadElementContentAs(itemType, null); else { o = Activator.CreateInstance(itemType); ReadObject(reader, o, handler); } if (o != null) output.Add(o); } } reader.ReadEndElement(); if (output.Count > 0) { System.Collections.IEnumerable par = output; Type et = typeof(object); if (dict.Count == 1) { foreach (var v in dict.Values) { et = v; break; } } /*else { Type t1 = output[0].GetType(); bool same = true; foreach (var item in output) if (item.GetType() != t1) { same = false; break; } if (same) et = t1; } if (et != typeof(object)) { Array ao = Array.CreateInstance(et, output.Count); for (int i = 0; i < output.Count; i++) ao.SetValue(output[i], i); par = ao; } else par = output.ToArray();*/ bool done = false; if (pi.PropertyType == par.GetType() || (pi.PropertyType.IsArray && (pi.PropertyType.GetElementType() == typeof(object) || pi.PropertyType.GetElementType() == et))) try { pi.SetValue(obj, par, null); done = true; } catch { } if (!done) { var mi = pi.PropertyType.GetMethod("AddRange", new Type[] { typeof(System.Collections.IEnumerable) }); if (mi != null) try { mi.Invoke(pi.GetValue(obj, null), new object[] { par }); done = true; } catch { } } if (!done) { var mi = pi.PropertyType.GetMethod("Add", new Type[] { typeof(object) }); if (mi != null) try { foreach (var i in par) mi.Invoke(pi.GetValue(obj, null), new object[] { i }); done = true; } catch { } } if (!done && et != typeof(Object)) { var mi = pi.PropertyType.GetMethod("Add", new Type[] { et }); if (mi != null) try { foreach (var i in par) mi.Invoke(pi.GetValue(obj, null), new object[] { i }); done = true; } catch { } } // Throw error if not done } } else { object inst = pi.GetValue(obj, null) ?? Activator.CreateInstance(pi.PropertyType); if (inst == null) throw new InvalidOperationException($"Can't get instance of {pi.PropertyType.Name}."); ReadObject(reader, inst, handler); } } else { reader.Skip(); reader.MoveToContent(); } } } public static void ReadObject([NotNull] XmlReader reader, [NotNull] object obj, PropertyConversionHandler handler = null) { if (obj == null) throw new ArgumentNullException(nameof(obj)); reader.MoveToContent(); if (obj is IXmlSerializable) { ((IXmlSerializable)obj).ReadXml(reader); } else { object oVal = null; string oName = GetAttributeValue(obj.GetType(), typeof(XmlRootAttribute), "ElementName", true, ref oVal) ? oVal.ToString() : obj.GetType().Name; if (reader.LocalName != oName) throw new XmlException("XML element name does not match object."); if (!reader.IsEmptyElement) { reader.ReadStartElement(); reader.MoveToContent(); ReadObjectProperties(reader, obj, handler); reader.ReadEndElement(); } else reader.Skip(); } } public static void ReadObjectFromXmlText([NotNull] string xml, [NotNull] object obj, PropertyConversionHandler handler = null) { using (System.IO.StringReader sr = new System.IO.StringReader(xml)) { using (XmlReader reader = XmlReader.Create(sr)) { reader.MoveToContent(); ReadObject(reader, obj, handler); } } } public static string WriteObjectToXmlText([NotNull] object obj, PropertyConversionHandler handler = null) { StringBuilder sb = new StringBuilder(); using (XmlWriter writer = XmlWriter.Create(sb, new XmlWriterSettings() { Indent = true })) WriteObject(writer, obj, handler, true); return sb.ToString(); } } }