我希望能够编写没有名称空间、没有XML前导的缩进XML片段,行结尾为\n,每个片段使用XmlSerializer,所有片段使用一个XmlWriter实例.我怎么能做到这一点?

XmlSerializer.Serialize()在序列化到通用输出流时生成缩进输出,但它使用"\n\r"作为行尾,我找不到如何配置它.

我可以序列化到XmlWriter,可以对其进行详细配置,但似乎只有在输出完整的单个文档而不是多个文档片段时,配置才起作用,因为XmlSerializer将抛出异常ConformanceLevel.Fragment:

无法在使用ConformanceLevel.Fragment创建的编写器上调用WriteStartDocument

而且,如果我调用XmlWriter.WriteWhitespace("");作为变通方法,缩进将被禁用. 具体来说,如果我像这样创建一个XmlWriter:

XmlWriter xw = XmlWriter.Create(System.Console.Out, new XmlWriterSettings()
{
    ConformanceLevel = ConformanceLevel.Fragment,
    NamespaceHandling = NamespaceHandling.Default,
    NewLineChars = "\n",
    Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // supress BOM
    Indent = true,
    NewLineHandling = NewLineHandling.Replace,
    OmitXmlDeclaration = true,
    WriteEndDocumentOnClose = false,
    CloseOutput = false,
    CheckCharacters = false,
    NewLineOnAttributes = false,
});

MVE

https://dotnetfiddle.net/qGLIlL个 它确实允许我序列化多个对象,而无需 for each 对象创建新的XmlWriter.但没有凹痕.

<flyingMonkey name="Koko"><limbs><limb name="leg" /><limb name="arm" /><limb name="tail" /><limb name="wing" /></limbs></flyingMonkey>

<flyingMonkey name="New Name"><limbs><limb name="leg" /><limb name="arm" /><limb name="tail" /><limb name="wing" /></limbs></flyingMonkey>
public class Program {
    public static void Main()
    {
        XmlWriter xw = XmlWriter.Create(System.Console.Out, new XmlWriterSettings()
        {
            ConformanceLevel = ConformanceLevel.Fragment,
            NamespaceHandling = NamespaceHandling.Default,
            NewLineChars = "\n",
            Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // supress BOM
            Indent = true,
            NewLineHandling = NewLineHandling.Replace,
            OmitXmlDeclaration = true,
            WriteEndDocumentOnClose = false,
            CloseOutput = false,
            CheckCharacters = false,
            NewLineOnAttributes = false,
        });
        var noNamespace = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });

        // without this line I get an error:
        //   "无法在使用ConformanceLevel.Fragment创建的编写器上调用WriteStartDocument."
        xw.WriteWhitespace("");

        FlyingMonkey monkey = FlyingMonkey.Create();
        XmlSerializer ser = new XmlSerializer(typeof(FlyingMonkey), defaultNamespace: null);
        ser.Serialize(xw, monkey, noNamespace);
        xw.WriteWhitespace("\n\n");
        monkey.name = "New Name";
        ser.Serialize(xw, monkey, noNamespace);
    }
}

[System.Xml.Serialization.XmlTypeAttribute(TypeName = "flyingMonkey", Namespace=null)]
public class FlyingMonkey
{
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string name;

    public Limb[] limbs;

    public static FlyingMonkey Create() =>
        new FlyingMonkey()
        {
            name = "Koko",
            limbs = new Limb[]
            {
                new Limb() { name = "leg" }, new Limb() { name = "arm" },
                new Limb() { name = "tail" }, new Limb() { name = "wing" },
            }
        };
}

[System.Xml.Serialization.XmlTypeAttribute(TypeName = "limb", Namespace=null)]
public class Limb
{
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string name;
}

有几分管用:

XmlWriter xw = XmlWriter.Create(System.Console.Out, new XmlWriterSettings()
{
    ConformanceLevel = ConformanceLevel.Auto,
    NamespaceHandling = NamespaceHandling.Default,
    NewLineChars = "\n",
    Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // supress BOM
    Indent = true,
    NewLineHandling = NewLineHandling.Replace,
    OmitXmlDeclaration = true,
    WriteEndDocumentOnClose = false,
    CloseOutput = false,
    CheckCharacters = false,
    NewLineOnAttributes = false,
});
var noNamespace = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });

// This is not needed anymore. If I invoke that, it will kill indentation for some reason.
// xw.WriteWhitespace("");

FlyingMonkey monkey = FlyingMonkey.Create();
XmlSerializer ser = new XmlSerializer(typeof(FlyingMonkey), defaultNamespace: null);
ser.Serialize(xw, monkey, noNamespace);
// xw.WriteWhitespace("\n\n");
// monkey.name = "New Name";
// ser.Serialize(xw, monkey, noNamespace); // this second serialization throws InvalidOperationException

它以正确的行尾打印,但不允许您将另一个对象写入同一个XmlWriter实例.

<flyingMonkey name="Koko">
  <limbs>
    <limb name="leg" />
    <limb name="arm" />
    <limb name="tail" />
    <limb name="wing" />
  </limbs>
</flyingMonkey>

我想要重用我的XmlWriter的单个实例,因为我需要编写多达XmlWriterk个元素,并且 for each 元素创建XmlWriter会增加大量开销

推荐答案

您的基本问题是XmlSerializer被设计成序列化到单个格式良好的XML文档,而不是序列化到一系列片段.如果try 使用ConformanceLevel.Fragment序列化到XML,则会出现异常

无法在使用ConformanceLevel.Fragment创建的编写器上调用WriteStartDocument

In this answer to .net XmlSerialize throws "无法在使用ConformanceLevel.Fragment创建的编写器上调用WriteStartDocument", Wim Reymen identifies two workarounds:

  • 在第一次调用序列化之前调用XmlWriter.WriteWhitespace("").

    不幸的是,正如您已经注意到的,这禁用了缩进.发生这种情况的原因是XmlWriter disables indentation for mixed content和对写入空格的调用触发了XmlEncodedRawTextWriterIndent的混合内容检测(演示here).

  • 在第一次序列化之前调用XmlWriter.WriteComment("").

    虽然这不会禁用缩进,但它当然会写出您不想要的注释.

那么,您有什么解决办法的 Select 呢?

Firstly,正如您注意到的,您可以使用CloseOutput = false为每一项创建单独的XmlWriter.在你写的 comments 中这样做adds a lot of overhead, I need to be writing up to 100k elements, so was hoping to reuse the writer instance,但我建议你的个人资料来确保,因为这个解决办法是非常,非常简单的相比,替代方案.

假设您正在向Stream写入数据,您可以创建如下扩展方法:

public static partial class XmlExtensions
{
    static Encoding Utf8EncodingNoBom { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
    
    public static void SerializeFragmentsToXml<T>(this IEnumerable<T> enumerable, Stream stream, XmlSerializer? serializer = null, XmlSerializerNamespaces? ns = null)
    {
        var newLine = "\n";
        var newLineBytes = Utf8EncodingNoBom.GetBytes(newLine+newLine);
        
        var settings = new XmlWriterSettings()
        {
            NamespaceHandling = NamespaceHandling.Default,
            NewLineChars = newLine,
            Encoding = Utf8EncodingNoBom, // supress BOM
            Indent = true,
            NewLineHandling = NewLineHandling.Replace,
            OmitXmlDeclaration = true,
            WriteEndDocumentOnClose = false,
            CloseOutput = false, // Required to prevent the stream from being closed between items
            CheckCharacters = false,
            NewLineOnAttributes = false,
        };
        
        serializer ??= new XmlSerializer(typeof(T));
        ns ??= new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });

        bool first = true;
        foreach (var item in enumerable)
        {
            if (!first)
                stream.Write(newLineBytes);
            using (var xmlWriter = XmlWriter.Create(stream, settings))
                serializer.Serialize(xmlWriter, item, ns);
            first = false;
        }
    }
}

并按如下方式使用它:

var items = new [] { "Koko", "POCO", "Loco" }.Select(n => FlyingMonkey.Create(n));

using var stream = new MemoryStream(); // Replace with some FileStream when serializing to disk

var serializer = new XmlSerializer(typeof(FlyingMonkey), defaultNamespace: null);
items.SerializeFragmentsToXml(stream, serializer : serializer);

演示小提琴#1here.

Alternatively,如果您出于性能原因而使用XmlWriter,您将需要调用XmlWriter.WriteComment()来防止XmlSerializer中的异常,并在之后编辑掉不需要的注释,例如通过一些TextWriter decorator在它们被动态编写时将它们移除.

下面的扩展方法似乎可以做到这一点:

public static partial class XmlExtensions
{
    const string FirstCommentText = "first";
    const string FirstComment = $"<!--{FirstCommentText}-->";
    const string SubsequentCommentText = "subsequent";
    const string SubsequentComment = $"<!--{SubsequentCommentText}-->";
    
    static Encoding Utf8EncodingNoBom { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
    
    public static void SerializeFragmentsToXml<T>(this IEnumerable<T> enumerable, Stream stream, XmlSerializer? serializer = null, XmlSerializerNamespaces? ns = null)
    {
        string newLine = "\n";
        
        var settings = new XmlWriterSettings()
        {
            ConformanceLevel = ConformanceLevel.Fragment,
            NamespaceHandling = NamespaceHandling.Default,
            NewLineChars = newLine,
            Encoding = Utf8EncodingNoBom, // supress BOM
            Indent = true,
            NewLineHandling = NewLineHandling.Replace,
            OmitXmlDeclaration = true,
            WriteEndDocumentOnClose = false,
            CloseOutput = false, 
            CheckCharacters = false,
            NewLineOnAttributes = false,
        };
        
        serializer ??= new XmlSerializer(typeof(T));
        ns ??= new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });

        using var innerTextWriter = new StreamWriter(stream, encoding : Utf8EncodingNoBom, leaveOpen  : true) { NewLine = newLine };
        using var textWriter = new FakeCommentRemovingTextWriter(innerTextWriter, new(FirstComment, ""), new(SubsequentComment, newLine)) { NewLine = newLine };
        using var xmlWriter = XmlWriter.Create(textWriter, settings);

        bool first = true;
        foreach (var item in enumerable)
        {
            xmlWriter.WriteComment(first ? FirstCommentText : SubsequentCommentText);
            serializer.Serialize(xmlWriter, item, ns);
            // XmlWriter buffers its output, so Flush() is required  to ensure that the fake comments are not split across calls to Write().
            xmlWriter.Flush(); 
            first = false;
        }
    }
    
    private class FakeCommentRemovingTextWriter : TextWriterDecorator
    {
        readonly KeyValuePair<string, string> [] replacements;
        
        public FakeCommentRemovingTextWriter(TextWriter baseWriter, params KeyValuePair<string, string> [] replacements) : base(baseWriter, true) => this.replacements = replacements;
        
        public override void Write(ReadOnlySpan<char> buffer)
        {
            foreach (var replacement in replacements)
            {
                int index;
                if ((index = StartsWithIgnoringWhitespace(buffer, replacement.Key)) >= 0)
                {
                    if (index > 0)
                        base.Write(buffer.Slice(0, index));
                    buffer = buffer.Slice(index).Slice(replacement.Key.Length);
                    if (buffer.StartsWith(NewLine))
                        buffer = buffer.Slice(NewLine.Length);
                    if (!string.IsNullOrEmpty(replacement.Value))
                        base.Write(replacement.Value);
                }
            }
            base.Write(buffer);
        }
        
        static int StartsWithIgnoringWhitespace(ReadOnlySpan<char> buffer, ReadOnlySpan<char> value)
        {
            for (int index = 0; index < buffer.Length; index++)
            {
                if (buffer.Slice(index).StartsWith(value))
                    return index;
                if (!XmlConvert.IsWhitespaceChar(buffer[index]) || index >= buffer.Length - value.Length)
                    break;
            }
            return -1;
        }
    }
}

public class TextWriterDecorator : TextWriter
{
    // Override the same methods that are overridden in https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/StringWriter.cs.
    TextWriter? baseWriter; // null when disposed
    readonly bool disposeBase;
    readonly Encoding baseEncoding;

    public TextWriterDecorator(TextWriter baseWriter, bool disposeBase = true) => 
        (this.baseWriter, this.disposeBase, this.baseEncoding) = (baseWriter ?? throw new ArgumentNullException(nameof(baseWriter)), disposeBase, baseWriter.Encoding);

    protected TextWriter BaseWriter => baseWriter == null ? throw new ObjectDisposedException(GetType().Name) : baseWriter;
    public override Encoding Encoding => baseEncoding;
    public override IFormatProvider FormatProvider => baseWriter?.FormatProvider ?? base.FormatProvider;
    [AllowNull] public override string NewLine 
    { 
        get => baseWriter?.NewLine ?? base.NewLine; 
        set
        {   
            if (baseWriter != null)
                baseWriter.NewLine = value;
            base.NewLine = value;
        }
    }

    public override void Flush() => BaseWriter.Flush();
    public sealed override void Close() => Dispose(true);
    public override void Write(char value) => BaseWriter.Write(value);
    public sealed override void Write(char[] buffer, int index, int count) => this.Write(buffer.AsSpan(index, count));
    public override void Write(ReadOnlySpan<char> buffer) => BaseWriter.Write(buffer);
    public sealed override void Write(string? value) => Write(value.AsSpan());

    public override Task WriteAsync(char value) => BaseWriter.WriteAsync(value);
    public sealed override Task WriteAsync(string? value) => WriteAsync(value.AsMemory());
    public sealed override Task WriteAsync(char[] buffer, int index, int count) => WriteAsync(buffer.AsMemory(index, count));
    public override Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) => BaseWriter.WriteAsync(buffer, cancellationToken);
    //public virtual Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default) - no need to override

    public override Task WriteLineAsync(char value) => BaseWriter.WriteLineAsync(value);
    public sealed override Task WriteLineAsync(string? value) => WriteLineAsync(value.AsMemory());
    public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) => BaseWriter.WriteLineAsync(value, cancellationToken);
    public sealed override Task WriteLineAsync(char[] buffer, int index, int count) => WriteLineAsync(buffer.AsMemory(index, count));
    public override Task WriteLineAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) => BaseWriter.WriteLineAsync(buffer, cancellationToken);
    
    public override Task FlushAsync() => BaseWriter.FlushAsync();
    public override Task FlushAsync(CancellationToken cancellationToken) => BaseWriter.FlushAsync(cancellationToken);

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing)
            {
                if (Interlocked.Exchange(ref this.baseWriter, null) is {} writer)
                    if (disposeBase)
                        writer.Dispose();
                    else
                        writer.Flush();
            }
        }
        finally
        {
            base.Dispose(disposing);
        }
    }

    public override async ValueTask DisposeAsync()
    {
        try
        {
            if (Interlocked.Exchange(ref this.baseWriter, null) is {} writer)
                if (disposeBase)
                    await writer.DisposeAsync().ConfigureAwait(false);
                else
                    await writer.FlushAsync().ConfigureAwait(false);
        }
        finally
        {
            await base.DisposeAsync().ConfigureAwait(false);
        }
    }
    
    public override string ToString() => string.Format("{0}: {1}", GetType().Name, baseWriter?.ToString() ?? "disposed");
}

但老实说,我觉得不值得这么麻烦.演示小提琴#2here.

无论采用哪种方法,输出都如下所示

<flyingMonkey name="Koko">
  <limbs>
    <limb name="leg" />
    <limb name="arm" />
    <limb name="tail" />
    <limb name="wing" />
  </limbs>
</flyingMonkey>

<flyingMonkey name="POCO">
  <limbs>
    <limb name="leg" />
    <limb name="arm" />
    <limb name="tail" />
    <limb name="wing" />
  </limbs>
</flyingMonkey>

<flyingMonkey name="Loco">
  <limbs>
    <limb name="leg" />
    <limb name="arm" />
    <limb name="tail" />
    <limb name="wing" />
  </limbs>
</flyingMonkey>

Csharp相关问答推荐

利用.NET 8中的AddStandardResilienceDeliveries和AddStandardHedgingDeliveries实现Resiliency

当Visual Studio处于升级管理模式时,无法安装Torch运行时

如何使用C#中的图形API更新用户配置文件图像

如何从泛型方法返回一个可为空的T,其中T:notnull?

当通过Google的Gmail Api发送邮件时,签名会产生dkim = neutral(正文散列未验证)'

(乌龙)1&#比c#中的UL&#慢吗?

如何定义EFCore中的多个穿透

从依赖项容器在.NET 8中的Program.cs文件中添加IOC

为什么@rendermode Interactive Auto不能在.NET 8.0 Blazor中运行?

对于PowerShell中的ConvertTo-SecureString方法,Microsoft如何将初始化向量添加到AES加密中的安全字符串?

如何在GRPC代码First服务的返回类型上使用多态性?

如何在mediatr命令中访问HttpContext而不安装弃用的nuget包

如何在使用属性 Select 器时判断是否可以为空

如何使用实体框架核心对字符串_agg使用强制转换为varchar(Max)

使用Blazor WebAssembly提高初始页面加载时间的性能

我应该为C#12中的主构造函数参数创建私有属性吗?

删除MudRadio时,MudRadioGroup未 Select 正确的MudRadio

从HTML元素获取 colored颜色

.NET EF Core Automapper项目到筛选不起作用

在使用xUnit和Mock执行单元测试时,控制器ViewResult返回空的Model集合