简体   繁体   English

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

[英]C# Xml deserialize XmlAttribute by value of attribute

I've been trying to read an oddly constructed XML file and have come up against a wall. 我一直在尝试读取一个奇怪构造的XML文件,并且碰到了墙。 I need to deserialize the XML into data structures in my C# program. 我需要在C#程序中将XML反序列化为数据结构。 A sample of the XML being: 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>

And my C# structures are as follows: 我的C#结构如下:

A class to allow me to grab the value of the "data" attribute 一个允许我获取“数据”属性值的类

    public class Data<T>
    {
        T dataAttr;

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

A structure to contain the entire "project" object 包含整个“项目”对象的结构

    [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; }
    }

A structure to contain the fields within the the tag 标记内包含字段的结构

    [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; }
    }

The issue is that the "name" attribute of the is not what is registered as the name of the node and therefor the syntax of [XmlElement(ElementName = "coordinate-system-internal")] doesn't work. 问题是的“名称”属性不是注册为节点名称的属性,因此[XmlElement(ElementName =“ coordinate-system-internal”)]的语法不起作用。 I'm stumped. 我很沮丧 Basically what I need to do is be able to specify the value of the "name" attribute as the way to deserialize the into the different project fields, storing the "data" value within the specified variable. 基本上,我需要做的是能够指定“名称”属性的值,以将其反序列化到不同的项目字段中,从而将“数据”值存储在指定变量中。

Your problem is that you have a sequence of elements like the following: 您的问题是您有一系列类似于以下内容的元素:

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

And you would like to interpret them as polymorphically specifying different types of values, depending upon the value of the kind attribute. 并且您希望将它们解释为多态地指定不同类型的值,具体取决于kind属性的值。

Unfortunately, out of the box XmlSerializer does not support polymorphic deserialization using an arbitrary attribute such as kind . 不幸的是, XmlSerializer不支持使用诸如kind的任意属性进行多态反序列化。 Instead it supports using the w3c standard attribute xsi:type to specify element type, as explained in the docs . 相反,它支持使用w3c标准属性xsi:type来指定元素类型,如docs中所述。

However, if we simply consider the attribute data to be a string, the XML for each <field> element actually has a fixed schema, with the unit attribute being optional. 但是,如果仅将属性data视为字符串,则每个<field>元素的XML实际上具有固定的架构,其中unit属性是可选的。 Thus you could deserialize your XML into the following DTO and then later convert the field values into their expected types: 因此,您可以将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); }
}

Sample fiddle #1 . 样本小提琴#1

However, from your question it appears you would like some (semi-)automatic way from within serialization to convert typed properties of ac# object from and to a list of <field name =...> elements. 但是,从您的问题看来,您希望通过某种(半)自动方式从序列化内将ac#对象的类型化属性转换为<field name =...>元素的列表,并将其转换为该列表。 Since this is not supported out of the box you will need to add a surrogate RPPProjectFieldDTO [] property to each type you wish to serialize in this manner, and handle the conversions inside the property getters and setters. 由于开箱即用不支持此功能,因此您需要向要以这种方式序列化的每种类型添加代理RPPProjectFieldDTO []属性,并处理属性getter和setter中的转换。 The following would be one prototype implementation: 以下是一个原型实现:

[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; }
}

Notes: 笔记:

  • The conventional, typed properties of RPPProjectFields are all marked with [XmlIgnore] . RPPProjectFields的常规类型属性都标记为[XmlIgnore] Instead there is a single surrogate property 相反,只有一个代理属性

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

    that is serialized. 被序列化。

  • Inside the surrogate property, reflection is used to cycle through all the "regular" properties of RPPProjectFields and convert them to objects of type RPPProjectFieldDTO . 在代理属性内,反射用于循环RPPProjectFields所有“常规”属性并将其转换为RPPProjectFields类型的RPPProjectFieldDTO However the XML names (eg "max-longitude" ) contain a character - that is illegal for use in ac# identifier. 但是,XML名称(例如"max-longitude" )包含一个字符-在ac#标识符中使用是非法的。 Thus it's necessary to specify an alternate name, but the properties cannot be marked with [XmlElement("Alternate Name")] since they are already marked with [XmlIgnore] . 因此,有必要指定一个备用名称,但是属性不能用[XmlElement("Alternate Name")]标记,因为它们已经用[XmlIgnore]标记了。 So I instead used data contract attributes to specify the alternate names. 因此,我改为使用数据协定属性来指定备用名称。

  • Some of your double values have units, eg <field name="min-altitude" data="550.094" kind="double" unit="m"/> To handle this I introduced a container struct DimensionalValue . 您的某些double值具有单位,例如<field name="min-altitude" data="550.094" kind="double" unit="m"/>为了处理此问题,我引入了一个容器结构DimensionalValue The idea would be to follow the quantity pattern (sometimes called the money pattern ) using this struct. 想法是使用此结构遵循数量模式 (有时称为货币模式 )。

Sample fiddle #2 which, sadly, doesn't compile because .NET Fiddle doesn't support System.Runtime.Serialization.dll. 样本小提琴#2 ,可惜由于.NET小提琴不支持System.Runtime.Serialization.dll而无法编译。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM