C#Xml按属性值反序列化XmlAttribute

时间:2017-11-02 15:26:35

标签: c# xml serialization

我一直试图读取一个奇怪构造的XML文件并且遇到了问题。我需要在我的C#程序中将XML反序列化为数据结构。 XML的一个示例是:

<object name="CAU_17_163" kind="project" states="expanded">
  <fields>
    <field name="coordinate-system-internal" data="WGS84" kind="string"/>
    <field name="min-longitude" data="-67.55643521" kind="double"/>
    <field name="min-latitude" data="45.09374232" kind="double"/>
    <field name="min-altitude" data="550.094" kind="double" unit="m"/>
    <field name="max-longitude" data="-66.52992272" kind="double"/>
    <field name="max-latitude" data="45.86876855" kind="double"/>
    <field name="max-altitude" data="1400.954" kind="double" unit="m"/>
    <field name="pop" data="0.917266016 0.398275101 0.000000000 0.000000000 -0.285868639 0.658383080 0.696283592 0.000000000 0.277312418 -0.638677277 0.717766786 0.000000000 1771794.580394641 -4080614.005124380 4555229.910096285 1.000000000 " kind="double[4][4]"/>
    <field name="pop-acquisition" data="0.917266016 0.398275101 0.000000000 0.000000000 -0.285868639 0.658383080 0.696283592 0.000000000 0.277312418 -0.638677277 0.717766786 0.000000000 1771794.580394641 -4080614.005124380 4555229.910096285 1.000000000 " kind="double[4][4]"/>
  </fields>

我的C#结构如下:

允许我抓住&#34;数据&#34;的一个类。属性

    public class Data<T>
    {
        T dataAttr;

        [XmlAttribute("data")]
        public T DataAttr { get => dataAttr; set => dataAttr = value; }
    }

包含整个&#34;项目的结构&#34;对象

    [XmlRoot("object")]
    public struct RPPProject
    {
        string name;
        RPPProjectFields fields;

        [XmlAttribute("name")]
        public string Name { get => name; set => name = value; }
        [XmlAttribute("fields")]
        public RPPProjectFields Fields { get => fields; set => fields = value; }
    }

包含标记

中字段的结构
    [XmlRoot("fields")]
    public struct RPPProjectFields
    {
        //Project fields
        Data<string> coordinate_system_internal;
        Data<double> min_longitude;
        Data<double> min_latitude;
        Data<double> min_altitude;
        Data<double> max_longitude;
        Data<double> max_latitude;
        Data<double> max_altitude;
        Data<double[]> pop; //[4][4]
        Data<double[]> pop_acquisition; //[4][4]

        [XmlElement(ElementName = "coordinate-system-internal")]
        public Data<string> Coordinate_system_internal { get => coordinate_system_internal; set => coordinate_system_internal = value; }
        [XmlElement(ElementName = "min-longitude")]
        public Data<double> Min_longitude { get => min_longitude; set => min_longitude = value; }
        [XmlElement(ElementName = "min-latitude")]
        public Data<double> Min_latitude { get => min_latitude; set => min_latitude = value; }
        [XmlElement(ElementName = "min-altitude")]
        public Data<double> Min_altitude { get => min_altitude; set => min_altitude = value; }
        [XmlElement(ElementName = "max-longitude")]
        public Data<double> Max_longitude { get => max_longitude; set => max_longitude = value; }
        [XmlElement(ElementName = "max-latitude")]
        public Data<double> Max_latitude { get => max_latitude; set => max_latitude = value; }
        [XmlElement(ElementName = "max-altitude")]
        public Data<double> Max_altitude { get => max_altitude; set => max_altitude = value; }
        [XmlElement(ElementName = "pop")]
        public Data<double[]> Pop { get => pop; set => pop = value; }
        [XmlElement(ElementName = "pop-acquisition")]
        public Data<double[]> Pop_acquisition { get => pop_acquisition; set => pop_acquisition = value; }
    }

问题在于&#34;名称&#34;该属性不是注册为节点名称的属性,因此[XmlElement(ElementName =&#34; coordinate-system-internal&#34;)]的语法不起作用。我很难过。基本上我需要做的是能够指定&#34; name&#34;的值。属性作为将反序列化到不同项目字段的方式,存储&#34;数据&#34;指定变量中的值。

1 个答案:

答案 0 :(得分:0)

您的问题是您有一系列元素,如下所示:

<field name="min-longitude" data="-67.55643521" kind="double"/>

并且您希望将它们解释为多态指定不同类型的值,具体取决于kind属性的值。

不幸的是,开箱即用XmlSerializer不支持使用kind之类的任意属性进行多态反序列化。相反,它支持使用w3c标准属性xsi:type来指定元素类型,如docs中所述。

但是,如果我们只是将属性data视为字符串,则每个<field>元素的XML实际上都具有固定架构,unit属性是可选的。因此,您可以将XML反序列化为以下DTO,然后将字段值转换为其预期类型:

public abstract class RPPItemBase
{
    [XmlAttribute("name")]
    public string Name { get; set; }

    [XmlAttribute("kind")]
    public string Kind { get; set; }

    bool ShouldSerializeKind() { return !string.IsNullOrEmpty(Kind); }
}

[XmlRoot("object")]
public class RPPProjectDTO : RPPItemBase
{
    public RPPProjectDTO() { this.Kind = "project"; }

    [XmlAttribute("states")]
    public string States { get; set; }

    [XmlElement("fields")]
    public RPPProjectFieldsDTO Fields { get; set; }
}

[XmlRoot("fields")]
public class RPPProjectFieldsDTO
{
    [XmlElement("field")]
    public RPPProjectFieldDTO[] Fields { get; set; }
}

public class RPPProjectFieldDTO : RPPItemBase
{
    [XmlAttribute("data")]
    public string Data { get; set; }

    [XmlAttribute("unit")]
    public string Unit { get; set; }

    public bool ShouldSerializeUnit() { return !string.IsNullOrEmpty(Unit); }
}

示例fiddle #1

但是,根据您的问题,您似乎希望在序列化中使用某种(半)自动方式将c#对象的类型属性转换为<field name =...>元素列表。由于不支持开箱即用,因此您需要为您希望以这种方式序列化的每种类型添加代理RPPProjectFieldDTO []属性,并处理属性getter和setter中的转换。以下是一个原型实现:

[XmlRoot("object")]
public class RPPProject : RPPItemBase
{
    public RPPProject() { this.Kind = "project"; }

    [XmlAttribute("states")]
    public string States { get; set; }

    [XmlElement("fields")]
    public RPPProjectFields Fields { get; set; }
}

[XmlRoot("fields")]
[DataContract(Name = "fields")]
public class RPPProjectFields
{
    [XmlIgnore]
    [DataMember(Name = "coordinate-system-internal")]
    public string CoordinateSystemInternal { get; set; }

    [XmlIgnore]
    [DataMember(Name = "min-longitude")]
    public double MinLongitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "min-latitude")]
    public double MinLatitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "min-altitude")]
    public DimensionalValue MinAltitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "max-longitude")]
    public double MaxLongitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "max-latitude")]
    public double MaxLatitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "max-altitude")]
    public DimensionalValue MaxAltitude { get; set; }

    [XmlIgnore]
    [DataMember(Name = "pop")]
    public double[][] Pop { get; set; } //[4][4]

    [XmlIgnore]
    [DataMember(Name = "pop-acquisition")]
    public double[][] PopAcquisition { get; set; } //[4][4]

    [XmlElement("field")]
    public RPPProjectFieldDTO[] Fields
    {
        get
        {
            return this.GetDataContractFields();
        }
        set
        {
            this.SetDataContractFields(value);
        }
    }
}

public enum Units
{
    [XmlEnum("none")]
    None,
    [XmlEnum("m")]
    Meters,
    [XmlEnum("cm")]
    Centimeters,
    [XmlEnum("mm")]
    Millimeters,
}

// Going with something like the Money Pattern here:
// http://www.dsc.ufcg.edu.br/~jacques/cursos/map/recursos/fowler-ap/Analysis%20Pattern%20Quantity.htm
// You may want to implement addition, subtraction, comparison and so on.

public struct DimensionalValue
{
    readonly Units units;
    readonly double value;

    public DimensionalValue(Units units, double value)
    {
        this.units = units;
        this.value = value;
    }

    public Units Units { get { return units; } }

    public double Value { get { return value; } }
}

public interface IFieldDTOParser
{
    Regex Regex { get; }

    bool TryCreateDTO(object obj, out RPPProjectFieldDTO field);

    object Parse(RPPProjectFieldDTO field, Match match);
}

class StringParser : IFieldDTOParser
{
    readonly Regex regex = new Regex("^string$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);

    #region IFieldDTOParser Members

    public Regex Regex { get { return regex; } }

    public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
    {
        if (obj is string)
        {
            field = new RPPProjectFieldDTO { Data = (string)obj, Kind = "string"};
            return true;
        }
        field = null;
        return false;
    }

    public object Parse(RPPProjectFieldDTO field, Match match)
    {
        return field.Data;
    }

    #endregion
}

class DoubleParser : IFieldDTOParser
{
    readonly Regex regex = new Regex("^double$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);

    #region IFieldDTOParser Members

    public Regex Regex { get { return regex; } }

    public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
    {
        if (obj is double)
        {
            field = new RPPProjectFieldDTO { Data = XmlConvert.ToString((double)obj), Kind = "double"};
            return true;
        }
        else if (obj is DimensionalValue)
        {
            var value = (DimensionalValue)obj;

            field = new RPPProjectFieldDTO { Data = XmlConvert.ToString(value.Value), Kind = "double", Unit = value.Units.ToXmlValue() };
            return true;
        }
        field = null;
        return false;
    }

    public object Parse(RPPProjectFieldDTO field, Match match)
    {
        var value = XmlConvert.ToDouble(field.Data);
        if (string.IsNullOrEmpty(field.Unit))
            return value;
        var unit = field.Unit.FromXmlValue<Units>();
        return new DimensionalValue(unit, value);

    }

    #endregion
}

class Double2DArrayParser : IFieldDTOParser
{
    readonly Regex regex = new Regex("^double\\[([0-9]+)\\]\\[([0-9]+)\\]$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);

    #region IFieldDTOParser Members

    public Regex Regex { get { return regex; } }

    public bool TryCreateDTO(object obj, out RPPProjectFieldDTO field)
    {
        if (obj is double[][])
        {
            var value = (double[][])obj;
            var nCols = value.GetLength(0);
            var rowLengths = value.Select(a => a == null ? 0 : a.Length).Distinct().ToArray();
            if (rowLengths.Length == 0)
            {
                field = new RPPProjectFieldDTO { Data = "", Kind = string.Format("double[{0}][{1}]", XmlConvert.ToString(nCols), "0")};
                return true;
            }
            else if (rowLengths.Length == 1)
            {
                field = new RPPProjectFieldDTO 
                { 
                    Data = String.Join(" ", value.SelectMany(a => a).Select(v => XmlConvert.ToString(v))),
                    Kind = string.Format("double[{0}][{1}]", XmlConvert.ToString(nCols), XmlConvert.ToString(rowLengths[0])) 
                };
                return true;
            }
        }
        field = null;
        return false;
    }

    public object Parse(RPPProjectFieldDTO field, Match match)
    {
        var nRows = XmlConvert.ToInt32(match.Groups[1].Value);
        var nCols = XmlConvert.ToInt32(match.Groups[2].Value);

        var array = new double[nRows][];

        var values = field.Data.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        for (int iRow = 0, iValue = 0; iRow < nRows; iRow++)
        {
            array[iRow] = new double[nCols];
            for (int iCol = 0; iCol < nCols; iCol++)
            {
                if (iValue < values.Length)
                    array[iRow][iCol] = XmlConvert.ToDouble(values[iValue++]);
            }
        }

        return array;
    }

    #endregion
}

public static class FieldDTOExtensions
{
    readonly static IFieldDTOParser[] parsers = new IFieldDTOParser[]
    {
        new StringParser(),
        new DoubleParser(),
        new Double2DArrayParser(),
    };

    public static void SetDataContractFields<T>(this T @this, RPPProjectFieldDTO [] value)
    {
        if (value == null)
            return;
        var lookup = value.ToDictionary(f => f.Name, f => f.Parse<object>());
        var query = from p in @this.GetType().GetProperties()
                    where p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0
                    let a = p.GetCustomAttributes<DataMemberAttribute>().SingleOrDefault()
                    where a != null
                    select new { Property = p, Name = a.Name };
        foreach (var property in query)
        {
            object item;
            if (lookup.TryGetValue(property.Name, out item))
            {
                property.Property.SetValue(@this, item, null);
            }
        }
    }

    public static RPPProjectFieldDTO[] GetDataContractFields<T>(this T @this)
    {
        var query = from p in @this.GetType().GetProperties()
                    where p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0
                    let a = p.GetCustomAttributes<DataMemberAttribute>().SingleOrDefault()
                    where a != null
                    let v = p.GetValue(@this, null)
                    where v != null
                    select FieldDTOExtensions.ToDTO(v, a.Name);
        return query.ToArray();
    }

    public static T Parse<T>(this RPPProjectFieldDTO field)
    {
        foreach (var parser in parsers)
        {
            var match = parser.Regex.Match(field.Kind);
            if (match.Success)
            {
                return (T)parser.Parse(field, match);
            }
        }
        throw new ArgumentException(string.Format("Unsupported object {0}", field.Kind));
    }

    public static RPPProjectFieldDTO ToDTO(object obj, string name)
    {
        RPPProjectFieldDTO field;
        foreach (var parser in parsers)
        {
            if (parser.TryCreateDTO(obj, out field))
            {
                field.Name = name;
                return field;
            }
        }
        throw new ArgumentException(string.Format("Unsupported object {0}", obj));
    }
}

// Taken from 
// https://stackoverflow.com/questions/42990069/get-element-of-an-enum-by-sending-xmlenumattribute-c

public static partial class XmlExtensions
{
    static XmlExtensions()
    {
        noStandardNamespaces = new XmlSerializerNamespaces();
        noStandardNamespaces.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd attributes.
    }

    readonly static XmlSerializerNamespaces noStandardNamespaces;
    internal const string RootNamespace = "XmlExtensions";
    internal const string RootName = "Root";

    public static TEnum FromXmlValue<TEnum>(this string xml) where TEnum : struct, IConvertible, IFormattable
    {
        var element = new XElement(XName.Get(RootName, RootNamespace), xml);
        return element.Deserialize<XmlExtensionsEnumWrapper<TEnum>>(null).Value;
    }

    public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
    {
        using (var reader = element.CreateReader())
        {
            object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
            if (result is T)
                return (T)result;
        }
        return default(T);
    }

    public static string ToXmlValue<TEnum>(this TEnum value) where TEnum : struct, IConvertible, IFormattable
    {
        var root = new XmlExtensionsEnumWrapper<TEnum> { Value = value };
        return root.SerializeToXElement().Value;
    }

    public static XElement SerializeToXElement<T>(this T obj)
    {
        return obj.SerializeToXElement(null, noStandardNamespaces); // Disable the xmlns:xsi and xmlns:xsd attributes by default.
    }

    public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
    {
        var doc = new XDocument();
        using (var writer = doc.CreateWriter())
            (serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
        var element = doc.Root;
        if (element != null)
            element.Remove();
        return element;
    }
}

[XmlRoot(XmlExtensions.RootName, Namespace = XmlExtensions.RootNamespace)]
[XmlType(IncludeInSchema = false)]
public class XmlExtensionsEnumWrapper<TEnum>
{
    [XmlText]
    public TEnum Value { get; set; }
}

注意:

  • RPPProjectFields的传统类型属性都标有[XmlIgnore]。相反,只有一个代理属性

    public RPPProjectFieldDTO[] Fields { get { ... } set { ... } } 
    

    是序列化的。

  • 在代理属性中,反射用于循环遍历RPPProjectFields的所有“常规”属性,并将它们转换为RPPProjectFieldDTO类型的对象。但是,XML名称(例如"max-longitude")包含在c#标识符中使用的字符-。因此,有必要指定备用名称,但不能使用[XmlElement("Alternate Name")]标记属性,因为它们已标记为[XmlIgnore]。所以我改为使用data contract attributes来指定备用名称。

  • 您的某些double值包含单位,例如<field name="min-altitude" data="550.094" kind="double" unit="m"/>为了解决这个问题,我引入了一个容器结构DimensionalValue。我们的想法是使用此结构跟随quantity pattern(有时称为money pattern)。

示例fiddle #2,遗憾的是,它不能编译,因为.NET Fiddle不支持System.Runtime.Serialization.dll。