Skip to content

Commit 6e22226

Browse files
committed
xml序列化性能优化
1 parent 74400a2 commit 6e22226

File tree

6 files changed

+354
-21
lines changed

6 files changed

+354
-21
lines changed

WebApiClientCore.Test/Attributes/ParameterAttributes/XmlContentAttributeTest.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
using System;
22
using System.Net.Http;
3+
using System.Text;
34
using System.Threading.Tasks;
5+
using System.Xml;
46
using WebApiClientCore.Attributes;
57
using WebApiClientCore.Implementations;
8+
using WebApiClientCore.Internals;
69
using WebApiClientCore.Serialization;
710
using Xunit;
811

912
namespace WebApiClientCore.Test.Attributes.ParameterAttributes
1013
{
1114
public class XmlContentAttributeTest
12-
{
15+
{
1316
public class Model
1417
{
1518
public string? name { get; set; }
@@ -29,12 +32,14 @@ public async Task OnRequestAsyncTest()
2932

3033
context.HttpContext.RequestMessage.RequestUri = new Uri("http://www.webapi.com/");
3134
context.HttpContext.RequestMessage.Method = HttpMethod.Post;
32-
35+
3336
var attr = new XmlContentAttribute();
3437
await attr.OnRequestAsync(new ApiParameterContext(context, 0));
35-
3638
var body = await context.HttpContext.RequestMessage.Content!.ReadAsStringAsync();
37-
var target = XmlSerializer.Serialize(context.Arguments[0],null);
39+
40+
using var bufferWriter = new RecyclableBufferWriter<byte>();
41+
XmlSerializer.Serialize(bufferWriter, context.Arguments[0], null);
42+
var target = Encoding.GetEncoding(attr.CharSet).GetString(bufferWriter.WrittenSpan);
3843
Assert.True(body == target);
3944
}
4045
}

WebApiClientCore/Attributes/ParameterAttributes/XmlContentAttribute.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ public string CharSet
3434
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
3535
protected override Task SetHttpContentAsync(ApiParameterContext context)
3636
{
37-
var xml = context.SerializeToXml(this.encoding);
38-
context.HttpContext.RequestMessage.Content = new XmlContent(xml, this.encoding);
37+
var options = context.HttpContext.HttpApiOptions.XmlSerializeOptions;
38+
if (encoding != null && encoding.Equals(options.Encoding) == false)
39+
{
40+
options = options.Clone();
41+
options.Encoding = encoding;
42+
}
43+
44+
context.HttpContext.RequestMessage.Content = new XmlContent(context.ParameterValue, options);
3945
return Task.CompletedTask;
4046
}
4147
}

WebApiClientCore/BuildinExtensions/EncodingExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace WebApiClientCore
88
/// <summary>
99
/// 提供Encoding扩展
1010
/// </summary>
11-
static class EncodingExtensions
11+
static partial class EncodingExtensions
1212
{
1313
/// <summary>
1414
/// 转换编码
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#if NETSTANDARD2_1
2+
using System;
3+
using System.Buffers;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Text;
7+
8+
namespace WebApiClientCore
9+
{
10+
/// <summary>
11+
/// 提供Encoding扩展
12+
/// </summary>
13+
static partial class EncodingExtensions
14+
{
15+
/// <summary>
16+
/// The maximum number of input elements after which we'll begin to chunk the input.
17+
/// </summary>
18+
/// <remarks>
19+
/// The reason for this chunking is that the existing Encoding / Encoder / Decoder APIs
20+
/// like GetByteCount / GetCharCount will throw if an integer overflow occurs. Since
21+
/// we may be working with large inputs in these extension methods, we don't want to
22+
/// risk running into this issue. While it's technically possible even for 1 million
23+
/// input elements to result in an overflow condition, such a scenario is unrealistic,
24+
/// so we won't worry about it.
25+
/// </remarks>
26+
private const int MaxInputElementsPerIteration = 1 * 1024 * 1024;
27+
28+
/// <summary>
29+
/// Encodes the specified <see cref="ReadOnlySpan{Char}"/> to <see langword="byte"/>s using the specified <see cref="Encoding"/>
30+
/// and writes the result to <paramref name="writer"/>.
31+
/// </summary>
32+
/// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="chars"/> should be encoded.</param>
33+
/// <param name="chars">The <see cref="ReadOnlySpan{Char}"/> to encode to <see langword="byte"/>s.</param>
34+
/// <param name="writer">The buffer to which the encoded bytes will be written.</param>
35+
/// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoding"/> is configured
36+
/// to throw an exception when such data is seen.</exception>
37+
public static long GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, IBufferWriter<byte> writer)
38+
{
39+
if (chars.Length <= MaxInputElementsPerIteration)
40+
{
41+
// The input span is small enough where we can one-shot this.
42+
43+
int byteCount = encoding.GetByteCount(chars);
44+
Span<byte> scratchBuffer = writer.GetSpan(byteCount);
45+
46+
int actualBytesWritten = encoding.GetBytes(chars, scratchBuffer);
47+
48+
writer.Advance(actualBytesWritten);
49+
return actualBytesWritten;
50+
}
51+
else
52+
{
53+
// Allocate a stateful Encoder instance and chunk this.
54+
55+
Convert(encoding.GetEncoder(), chars, writer, flush: true, out long totalBytesWritten, out _);
56+
return totalBytesWritten;
57+
}
58+
}
59+
60+
/// <summary>
61+
/// Decodes the specified <see cref="ReadOnlySpan{Byte}"/> to <see langword="char"/>s using the specified <see cref="Encoding"/>
62+
/// and writes the result to <paramref name="writer"/>.
63+
/// </summary>
64+
/// <param name="encoding">The <see cref="Encoding"/> which represents how the data in <paramref name="bytes"/> should be decoded.</param>
65+
/// <param name="bytes">The <see cref="ReadOnlySpan{Byte}"/> whose bytes should be decoded.</param>
66+
/// <param name="writer">The buffer to which the decoded chars will be written.</param>
67+
/// <returns>The number of chars written to <paramref name="writer"/>.</returns>
68+
/// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be decoded and <paramref name="encoding"/> is configured
69+
/// to throw an exception when such data is seen.</exception>
70+
public static long GetChars(this Encoding encoding, ReadOnlySpan<byte> bytes, IBufferWriter<char> writer)
71+
{
72+
if (bytes.Length <= MaxInputElementsPerIteration)
73+
{
74+
// The input span is small enough where we can one-shot this.
75+
76+
int charCount = encoding.GetCharCount(bytes);
77+
Span<char> scratchBuffer = writer.GetSpan(charCount);
78+
79+
int actualCharsWritten = encoding.GetChars(bytes, scratchBuffer);
80+
81+
writer.Advance(actualCharsWritten);
82+
return actualCharsWritten;
83+
}
84+
else
85+
{
86+
// Allocate a stateful Decoder instance and chunk this.
87+
88+
Convert(encoding.GetDecoder(), bytes, writer, flush: true, out long totalCharsWritten, out _);
89+
return totalCharsWritten;
90+
}
91+
}
92+
93+
/// <summary>
94+
/// Converts a <see cref="ReadOnlySpan{Char}"/> to bytes using <paramref name="encoder"/> and writes the result to <paramref name="writer"/>.
95+
/// </summary>
96+
/// <param name="encoder">The <see cref="Encoder"/> instance which can convert <see langword="char"/>s to <see langword="byte"/>s.</param>
97+
/// <param name="chars">A sequence of characters to encode.</param>
98+
/// <param name="writer">The buffer to which the encoded bytes will be written.</param>
99+
/// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
100+
/// <param name="bytesUsed">When this method returns, contains the count of <see langword="byte"/>s which were written to <paramref name="writer"/>.</param>
101+
/// <param name="completed">
102+
/// When this method returns, contains <see langword="true"/> if <paramref name="encoder"/> contains no partial internal state; otherwise, <see langword="false"/>.
103+
/// If <paramref name="flush"/> is <see langword="true"/>, this will always be set to <see langword="true"/> when the method returns.
104+
/// </param>
105+
/// <exception cref="EncoderFallbackException">Thrown if <paramref name="chars"/> contains data that cannot be encoded and <paramref name="encoder"/> is configured
106+
/// to throw an exception when such data is seen.</exception>
107+
public static void Convert(this Encoder encoder, ReadOnlySpan<char> chars, IBufferWriter<byte> writer, bool flush, out long bytesUsed, out bool completed)
108+
{
109+
// We need to perform at least one iteration of the loop since the encoder could have internal state.
110+
111+
long totalBytesWritten = 0;
112+
113+
do
114+
{
115+
// If our remaining input is very large, instead truncate it and tell the encoder
116+
// that there'll be more data after this call. This truncation is only for the
117+
// purposes of getting the required byte count. Since the writer may give us a span
118+
// larger than what we asked for, we'll pass the entirety of the remaining data
119+
// to the transcoding routine, since it may be able to make progress beyond what
120+
// was initially computed for the truncated input data.
121+
122+
int byteCountForThisSlice = (chars.Length <= MaxInputElementsPerIteration)
123+
? encoder.GetByteCount(chars, flush)
124+
: encoder.GetByteCount(chars.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */);
125+
126+
Span<byte> scratchBuffer = writer.GetSpan(byteCountForThisSlice);
127+
128+
encoder.Convert(chars, scratchBuffer, flush, out int charsUsedJustNow, out int bytesWrittenJustNow, out completed);
129+
130+
chars = chars.Slice(charsUsedJustNow);
131+
writer.Advance(bytesWrittenJustNow);
132+
totalBytesWritten += bytesWrittenJustNow;
133+
} while (!chars.IsEmpty);
134+
135+
bytesUsed = totalBytesWritten;
136+
}
137+
138+
139+
/// <summary>
140+
/// Converts a <see cref="ReadOnlySpan{Byte}"/> to chars using <paramref name="decoder"/> and writes the result to <paramref name="writer"/>.
141+
/// </summary>
142+
/// <param name="decoder">The <see cref="Decoder"/> instance which can convert <see langword="byte"/>s to <see langword="char"/>s.</param>
143+
/// <param name="bytes">A sequence of bytes to decode.</param>
144+
/// <param name="writer">The buffer to which the decoded chars will be written.</param>
145+
/// <param name="flush"><see langword="true"/> to indicate no further data is to be converted; otherwise <see langword="false"/>.</param>
146+
/// <param name="charsUsed">When this method returns, contains the count of <see langword="char"/>s which were written to <paramref name="writer"/>.</param>
147+
/// <param name="completed">
148+
/// When this method returns, contains <see langword="true"/> if <paramref name="decoder"/> contains no partial internal state; otherwise, <see langword="false"/>.
149+
/// If <paramref name="flush"/> is <see langword="true"/>, this will always be set to <see langword="true"/> when the method returns.
150+
/// </param>
151+
/// <exception cref="DecoderFallbackException">Thrown if <paramref name="bytes"/> contains data that cannot be encoded and <paramref name="decoder"/> is configured
152+
/// to throw an exception when such data is seen.</exception>
153+
public static void Convert(this Decoder decoder, ReadOnlySpan<byte> bytes, IBufferWriter<char> writer, bool flush, out long charsUsed, out bool completed)
154+
{
155+
// We need to perform at least one iteration of the loop since the decoder could have internal state.
156+
157+
long totalCharsWritten = 0;
158+
159+
do
160+
{
161+
// If our remaining input is very large, instead truncate it and tell the decoder
162+
// that there'll be more data after this call. This truncation is only for the
163+
// purposes of getting the required char count. Since the writer may give us a span
164+
// larger than what we asked for, we'll pass the entirety of the remaining data
165+
// to the transcoding routine, since it may be able to make progress beyond what
166+
// was initially computed for the truncated input data.
167+
168+
int charCountForThisSlice = (bytes.Length <= MaxInputElementsPerIteration)
169+
? decoder.GetCharCount(bytes, flush)
170+
: decoder.GetCharCount(bytes.Slice(0, MaxInputElementsPerIteration), flush: false /* this isn't the end of the data */);
171+
172+
Span<char> scratchBuffer = writer.GetSpan(charCountForThisSlice);
173+
174+
decoder.Convert(bytes, scratchBuffer, flush, out int bytesUsedJustNow, out int charsWrittenJustNow, out completed);
175+
176+
bytes = bytes.Slice(bytesUsedJustNow);
177+
writer.Advance(charsWrittenJustNow);
178+
totalCharsWritten += charsWrittenJustNow;
179+
} while (!bytes.IsEmpty);
180+
181+
charsUsed = totalCharsWritten;
182+
}
183+
}
184+
}
185+
186+
#endif
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
using System.Net.Http;
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Net.Http.Headers;
23
using System.Text;
4+
using System.Xml;
5+
using WebApiClientCore.Serialization;
36

47
namespace WebApiClientCore.HttpContents
58
{
69
/// <summary>
710
/// 表示 xml 内容
811
/// </summary>
9-
public class XmlContent : StringContent
12+
public class XmlContent : BufferContent
1013
{
14+
private static readonly MediaTypeHeaderValue defaultMediaType = new(MediaType) { CharSet = Encoding.UTF8.WebName };
15+
1116
/// <summary>
1217
/// 获取对应的ContentType
1318
/// </summary>
@@ -19,8 +24,22 @@ public class XmlContent : StringContent
1924
/// <param name="xml">xml内容</param>
2025
/// <param name="encoding">编码</param>
2126
public XmlContent(string? xml, Encoding encoding)
22-
: base(xml ?? string.Empty, encoding, MediaType)
2327
{
28+
encoding.GetBytes(xml, this);
29+
this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName };
30+
}
31+
32+
/// <summary>
33+
/// xml内容
34+
/// </summary>
35+
/// <param name="obj">xml实体</param>
36+
/// <param name="xmlWriterSettings">xml写入设置项</param>
37+
[RequiresUnreferencedCode("Members from serialized types may be trimmed if not referenced directly")]
38+
public XmlContent(object? obj, XmlWriterSettings xmlWriterSettings)
39+
{
40+
XmlSerializer.Serialize(this, obj, xmlWriterSettings);
41+
var encoding = xmlWriterSettings.Encoding;
42+
this.Headers.ContentType = encoding == Encoding.UTF8 ? defaultMediaType : new MediaTypeHeaderValue(MediaType) { CharSet = encoding.WebName };
2443
}
2544
}
26-
}
45+
}

0 commit comments

Comments
 (0)