我一直试图读取一个奇怪构造的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;指定变量中的值。
答案 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。