使用数据合同模型将 IParsable 序列化为字符串的最简单方法<T>是什么?



我想在需要DataContract序列化方法(readonly字段等)的更广泛序列化的上下文中将IParsable<MyType>序列化为<mns:MyType>MyType.ToString()</mns:MyType>

我已经尝试了几种方法,包括ISerializableISerializationSurrogate,但它们都导致了许多代码看起来与整个过程中使用的DataContract属性有着令人困惑的不同,这让我确信我使用了错误的工具。MyTypestring之间的转换应该很容易。最简单的方法是什么?

在.NET Core中为某些类型的MyType实现数据契约序列化代理的步骤如下:

  1. 创建一个代理类型,该代理类型可以通过数据协定序列化程序进行序列化,并且可以从原始类型MyType来回转换。

  2. 创建一个实现以下方法的代理提供程序类型ISerializationSurrogateProvider

    • GetSurrogateType(Type)-在传递typeof(MyType)时返回代理项类型。

    • GetObjectToSerialize(Object, Type)-在传递MyType的实例时返回代理项的实例。

    • GetDeserializedObject(Object, Type)-在传递代理项时返回MyType的实例。

  3. 在序列化或反序列化之前,构造您自己的DataContractSerializer并调用DataContractSerializerExtensions.SetSerializationSurrogateProvider(provider),以便在序列化程序中注册步骤#2中提供程序的实例。

重要注意事项

  • 在.NET Framework中使用数据协定代理的方法与.NET Core完全不同。如果您在.NET Framework中工作,请忽略此答案,并参阅数据契约代理。

  • ToString()方法中,请确保使用CultureInfo.InvariantCulture格式化所有数值和其他值。如果不这样做,在一个区域设置中生成的XML在另一个区域中将无法读取。如果出于某种原因需要本地化的ToString(),请使MyType实现IFormattable,并在序列化时使用ToString(default, CultureInfo.InvariantCulture)。(format字符串可以忽略。)

  • 您提到尝试将ISerializable用于您的MyType。这种技术将数据存储为XML序列化流中的元素/值对。由于所需的XML只包含文本而不包含标记元素,因此这种方法可能不合适。

考虑到这一点,我们可以为实现IParsable<TSelf>的任何类型创建一个通用代理,如下所示:

public sealed class ParsableSurrogate<TSelf> where TSelf : IParsable<TSelf>
{
[IgnoreDataMember]
public TSelf? Value { get; set; }
public string? TextValue { get { return Value?.ToString(); } set { Value = (value == null ? default : TSelf.Parse(value, default)); } }
}

接下来,制作以下ISerializationSurrogateProvider:

public class SerializationSurrogateProvider : ISerializationSurrogateProvider
{
readonly Dictionary<Type, (Type SurrogateType, Func<object, Type, object> ToSurrogate)> toSurrogateDictionary = new();
readonly Dictionary<Type, (Type OriginalType, Func<object, Type, object> ToOriginal)> fromSurrogateDictionary = new();

public Type GetSurrogateType(Type type) =>
toSurrogateDictionary.TryGetValue(type, out var entry) ? entry.SurrogateType : type;
// TODO: check to see whether null objects are handled correctly
public object GetObjectToSerialize(object obj, Type targetType) =>
toSurrogateDictionary.TryGetValue(obj.GetType(), out var entry) ? entry.ToSurrogate(obj, targetType) : obj;
public object GetDeserializedObject(object obj, Type targetType) =>
fromSurrogateDictionary.TryGetValue(obj.GetType(), out var entry) ? entry.ToOriginal(obj, targetType) : obj;

public SerializationSurrogateProvider AddParsable<TParsable>() where TParsable : IParsable<TParsable>
{
toSurrogateDictionary.Add(typeof(TParsable), (typeof(ParsableSurrogate<TParsable>), (obj, t) => new ParsableSurrogate<TParsable> { Value = (TParsable)obj }));
fromSurrogateDictionary.Add(typeof(ParsableSurrogate<TParsable>), (typeof(TParsable), (obj, t) => ((ParsableSurrogate<TParsable>)obj).Value!));
return this;
}
}

现在,假设您的MyType看起来像这样:

public class MyType : IParsable<MyType>
{
public MyType(string? value1, string? value2) => (this.Value1, this.Value2) = (value1, value2);

public string? Value1 { get; }
public string? Value2 { get; }

public override string ToString() => JsonSerializer.Serialize(this);

public static MyType Parse (string s, IFormatProvider? provider) => JsonSerializer.Deserialize<MyType>(s) ?? throw new ArgumentException();
public static bool TryParse (string? s, IFormatProvider? provider, out MyType result) => throw new NotImplementedException("not needed for the question");
}

然后,要从XML序列化到XML,您可以为包含MyType实例的任何模型类型创建一个DataContractSerializer,如下所示:

var serializer = new DataContractSerializer(model.GetType());
serializer.SetSerializationSurrogateProvider(new SerializationSurrogateProvider().AddParsable<MyType>());
var xml = model.ToContractXml(serializer : serializer);
var model2 = DataContractSerializerHelper.FromContractXml<Model>(xml, serializer);

使用扩展方法:

public static partial class DataContractSerializerHelper
{
public static string ToContractXml<T>(this T obj, DataContractSerializer? serializer = null, XmlWriterSettings? settings = null)
{
serializer = serializer ?? new DataContractSerializer(obj == null ? typeof(T) : obj.GetType());
using (var textWriter = new StringWriter())
{
settings = settings ?? new XmlWriterSettings { Indent = true };
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
{
serializer.WriteObject(xmlWriter, obj);
}
return textWriter.ToString();
}
}
public static T? FromContractXml<T>(string xml, DataContractSerializer? serializer = null)
{
using (var textReader = new StringReader(xml ?? ""))
using (var xmlReader = XmlReader.Create(textReader))
{
return (T?)(serializer ?? new DataContractSerializer(typeof(T))).ReadObject(xmlReader);
}
}
}

MyType是根模型本身时,生成的XML将如下所示:

<ParsableSurrogateOfMyTypeMTRdQN6P xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
<TextValue>{"Value1":"hello","Value2":"there"}</TextValue>
</ParsableSurrogateOfMyTypeMTRdQN6P>

在这里演示小提琴#1。

当这起作用时,你会注意到几个问题:

  1. 文本值嵌套在<TextValue>元素中。在你的问题中,你明确表示你不想那样。

    由于DataContractSerializer不支持与XmlSerializer[XmlText]属性等效的属性,因此有必要实现IXmlSerializable

  2. 根元素名称<ParsableSurrogateOfMyTypeMTRdQN6P>很愚蠢,与您的需求<mns:MyType>不匹配。

    由于我们将实现IXmlSerializable,因此有必要使用从这个答案到的解决方案。当使用数据协定序列化程序序列化IXmlSerializable对象时,我如何控制根元素命名空间和名称

这需要如下修改ParsableSurrogate<TSelf>

[XmlSchemaProvider("GetSchemaMethod")]
public sealed class ParsableSurrogate<TSelf> : IXmlSerializable where TSelf : IParsable<TSelf>
{
public TSelf? Value { get; set; }
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Value = TSelf.Parse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture);
}

public void WriteXml(XmlWriter writer) => writer.WriteValue(Value?.ToString());
public XmlSchema? GetSchema() => null;

const string DataContractNamespacePrefix = "http://schemas.datacontract.org/2004/07/";
static void GetDataContractNamespaceAndName(Type type, out string name, out string @namespace)
{
// TODO: tweak this logic as required to get the correct namespace and name
(name, @namespace) = (type.Name, DataContractNamespacePrefix + type.Namespace);
if (type.GetCustomAttribute<DataContractAttribute>() is {} attr)
(name, @namespace) = (attr.Name ?? name, attr.Namespace ?? @namespace);
}

// This is the method named by the XmlSchemaProviderAttribute applied to the type.
public static XmlQualifiedName GetSchemaMethod(XmlSchemaSet xs)
{
GetDataContractNamespaceAndName(typeof(TSelf), out var name, out var @namespace);
// Create the TSelf type as a restriction of string
var tSelfType = new XmlSchemaSimpleType
{
Name = name,
Content = new XmlSchemaSimpleTypeRestriction { BaseTypeName = XmlSchemaType.GetBuiltInSimpleType(XmlTypeCode.String).QualifiedName },
};
// Create the top-level TSelf element.
var tSelfElement = new XmlSchemaElement
{
Name = name,
SchemaTypeName = new XmlQualifiedName(tSelfType.Name, @namespace),
};
// Create a schema with the type & element
var tSelfSchema = new XmlSchema
{
TargetNamespace = @namespace,
// Add the top-level element and types to the schema
Items = { tSelfElement, tSelfType },
};
xs.Add(tSelfSchema);
return new XmlQualifiedName(name, @namespace);
}
}

并将所需的数据契约名称空间添加到MyType中,如下所示:

[DataContract(Namespace = "https://stackoverflow.com/questions/75912029")] // Replace with whatever namespace you need
public class MyType : IParsable<MyType>
{

完成此操作后,将为MyType生成以下XML:

<MyType xmlns="https://stackoverflow.com/questions/75912029">{"Value1":"hello","Value2":"there"}</MyType>

对于包含MyType列表的模型,如以下所示:

public class Model
{
public List<MyType?> MyTypes { get; set; } = new ();
}

根据需要生成以下XML:

<Model xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
<MyTypes xmlns:d2p1="https://stackoverflow.com/questions/75912029">
<d2p1:MyType>{"Value1":"hello","Value2":"there"}</d2p1:MyType>
<d2p1:MyType i:nil="true" />
<d2p1:MyType>{"Value1":"foo","Value2":"bar"}</d2p1:MyType>
</MyTypes>
</Model>

在这里演示小提琴#2。

相关内容

最新更新