Skip to content

Commit dcf79a4

Browse files
authored
Replace ATP pattern with async/await in SmtpClient (part 1) (#115366)
* Test improvements * SmtpCommands and SmtpReplyReader * MailMessage and friends * Remove allocations in SmtpCommands * More feedback
1 parent 9231f34 commit dcf79a4

24 files changed

+497
-1065
lines changed

src/libraries/System.Net.Mail/src/System.Net.Mail.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<Compile Include="System\Net\Mail\Attachment.cs" />
4848
<Compile Include="System\Net\Mail\AttachmentCollection.cs" />
4949
<Compile Include="System\Net\BufferedReadStream.cs" />
50+
<Compile Include="System\Net\Mail\ReadWriteAdapter.cs" />
5051
<Compile Include="System\Net\Mail\LinkedResource.cs" />
5152
<Compile Include="System\Net\Mail\LinkedResourceCollection.cs" />
5253
<Compile Include="System\Net\Mail\DomainLiteralReader.cs" />

src/libraries/System.Net.Mail/src/System/Net/BufferBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ internal void Append(byte value)
3434
_buffer[_offset++] = value;
3535
}
3636

37+
internal void Append(ReadOnlyMemory<byte> value)
38+
{
39+
EnsureBuffer(value.Length);
40+
value.Span.CopyTo(_buffer.AsSpan(_offset));
41+
_offset += value.Length;
42+
}
43+
3744
internal void Append(ReadOnlySpan<byte> value)
3845
{
3946
EnsureBuffer(value.Length);

src/libraries/System.Net.Mail/src/System/Net/Mail/MailMessage.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
using System;
55
using System.Collections.Specialized;
6+
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
89
using System.Net.Mime;
910
using System.Text;
11+
using System.Threading;
12+
using System.Threading.Tasks;
1013

1114
namespace System.Net.Mail
1215
{
@@ -429,22 +432,28 @@ private void SetContent(bool allowUnicode)
429432
}
430433
}
431434

432-
internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode)
435+
internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default)
433436
{
434-
SetContent(allowUnicode);
435-
_message.Send(writer, sendEnvelope, allowUnicode);
437+
Task task = SendAsync<SyncReadWriteAdapter>(writer, sendEnvelope, allowUnicode, cancellationToken);
438+
Debug.Assert(task.IsCompleted, "SendAsync should be completed synchronously.");
439+
task.GetAwaiter().GetResult();
436440
}
437441

438-
internal IAsyncResult BeginSend(BaseWriter writer, bool allowUnicode,
439-
AsyncCallback? callback, object? state)
442+
internal IAsyncResult BeginSend(BaseWriter writer, bool sendEnvelope, bool allowUnicode, AsyncCallback callback, object? state)
440443
{
441-
SetContent(allowUnicode);
442-
return _message.BeginSend(writer, allowUnicode, callback, state);
444+
return TaskToAsyncResult.Begin(SendAsync<AsyncReadWriteAdapter>(writer, sendEnvelope, allowUnicode), callback, state);
445+
}
446+
447+
internal static void EndSend(IAsyncResult asyncResult)
448+
{
449+
TaskToAsyncResult.End(asyncResult);
443450
}
444451

445-
internal void EndSend(IAsyncResult asyncResult)
452+
internal async Task SendAsync<TIOAdapter>(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default)
453+
where TIOAdapter : IReadWriteAdapter
446454
{
447-
_message.EndSend(asyncResult);
455+
SetContent(allowUnicode);
456+
await _message.SendAsync<TIOAdapter>(writer, sendEnvelope, allowUnicode, cancellationToken).ConfigureAwait(false);
448457
}
449458

450459
internal string BuildDeliveryStatusNotificationString()

src/libraries/System.Net.Mail/src/System/Net/Mail/MailPriority.cs

Lines changed: 9 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Net.Mime;
66
using System.Runtime.ExceptionServices;
77
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
810

911
namespace System.Net.Mail
1012
{
@@ -139,7 +141,9 @@ internal string? Subject
139141
// extract the encoding from =?encoding?BorQ?blablalba?=
140142
inputEncoding = MimeBasePart.DecodeEncoding(value);
141143
}
142-
catch (ArgumentException) { };
144+
catch (ArgumentException)
145+
{
146+
}
143147

144148
if (inputEncoding != null && value != null)
145149
{
@@ -240,94 +244,8 @@ internal MimeBasePart? Content
240244

241245
#region Sending
242246

243-
internal void EmptySendCallback(IAsyncResult result)
244-
{
245-
Exception? e = null;
246-
247-
if (result.CompletedSynchronously)
248-
{
249-
return;
250-
}
251-
252-
EmptySendContext context = (EmptySendContext)result.AsyncState!;
253-
try
254-
{
255-
BaseWriter.EndGetContentStream(result).Close();
256-
}
257-
catch (Exception ex)
258-
{
259-
e = ex;
260-
}
261-
context._result.InvokeCallback(e);
262-
}
263-
264-
internal sealed class EmptySendContext
265-
{
266-
internal EmptySendContext(BaseWriter writer, LazyAsyncResult result)
267-
{
268-
_writer = writer;
269-
_result = result;
270-
}
271-
272-
internal LazyAsyncResult _result;
273-
internal BaseWriter _writer;
274-
}
275-
276-
internal IAsyncResult BeginSend(BaseWriter writer, bool allowUnicode,
277-
AsyncCallback? callback, object? state)
278-
{
279-
PrepareHeaders(allowUnicode);
280-
writer.WriteHeaders(Headers, allowUnicode);
281-
282-
if (Content != null)
283-
{
284-
return Content.BeginSend(writer, callback, allowUnicode, state);
285-
}
286-
else
287-
{
288-
LazyAsyncResult result = new LazyAsyncResult(this, state, callback);
289-
IAsyncResult newResult = writer.BeginGetContentStream(EmptySendCallback, new EmptySendContext(writer, result));
290-
if (newResult.CompletedSynchronously)
291-
{
292-
BaseWriter.EndGetContentStream(newResult).Close();
293-
result.InvokeCallback();
294-
}
295-
return result;
296-
}
297-
}
298-
299-
internal void EndSend(IAsyncResult asyncResult)
300-
{
301-
ArgumentNullException.ThrowIfNull(asyncResult);
302-
303-
if (Content != null)
304-
{
305-
Content.EndSend(asyncResult);
306-
}
307-
else
308-
{
309-
LazyAsyncResult? castedAsyncResult = asyncResult as LazyAsyncResult;
310-
311-
if (castedAsyncResult == null || castedAsyncResult.AsyncObject != this)
312-
{
313-
throw new ArgumentException(SR.net_io_invalidasyncresult);
314-
}
315-
316-
if (castedAsyncResult.EndCalled)
317-
{
318-
throw new InvalidOperationException(SR.Format(SR.net_io_invalidendcall, nameof(EndSend)));
319-
}
320-
321-
castedAsyncResult.InternalWaitForCompletion();
322-
castedAsyncResult.EndCalled = true;
323-
if (castedAsyncResult.Result is Exception e)
324-
{
325-
ExceptionDispatchInfo.Throw(e);
326-
}
327-
}
328-
}
329-
330-
internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode)
247+
internal async Task SendAsync<TIOAdapter>(BaseWriter writer, bool sendEnvelope, bool allowUnicode, CancellationToken cancellationToken = default)
248+
where TIOAdapter : IReadWriteAdapter
331249
{
332250
if (sendEnvelope)
333251
{
@@ -340,10 +258,11 @@ internal void Send(BaseWriter writer, bool sendEnvelope, bool allowUnicode)
340258

341259
if (Content != null)
342260
{
343-
Content.Send(writer, allowUnicode);
261+
await Content.SendAsync<TIOAdapter>(writer, allowUnicode, cancellationToken).ConfigureAwait(false);
344262
}
345263
else
346264
{
265+
// No content to write, just close the stream
347266
writer.GetContentStream().Close();
348267
}
349268
}

src/libraries/System.Net.Mail/src/System/Net/Mail/MailWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal override void WriteHeaders(NameValueCollection headers, bool allowUnico
4040
internal override void Close()
4141
{
4242
_bufferBuilder.Append("\r\n"u8);
43-
Flush(null);
43+
Flush();
4444
_stream.Close();
4545
}
4646

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace System.Net.Mail
9+
{
10+
internal interface IReadWriteAdapter
11+
{
12+
static abstract ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken);
13+
static abstract ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken);
14+
static abstract ValueTask WriteAsync(Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken);
15+
static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken);
16+
static abstract Task WaitAsync(TaskCompletionSource<bool> waiter);
17+
}
18+
19+
internal readonly struct AsyncReadWriteAdapter : IReadWriteAdapter
20+
{
21+
public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
22+
stream.ReadAsync(buffer, cancellationToken);
23+
24+
public static ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) =>
25+
stream.ReadAtLeastAsync(buffer, minimumBytes, throwOnEndOfStream, cancellationToken);
26+
27+
public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken) =>
28+
stream.WriteAsync(buffer, cancellationToken);
29+
30+
public static Task FlushAsync(Stream stream, CancellationToken cancellationToken) => stream.FlushAsync(cancellationToken);
31+
32+
public static Task WaitAsync(TaskCompletionSource<bool> waiter) => waiter.Task;
33+
}
34+
35+
internal readonly struct SyncReadWriteAdapter : IReadWriteAdapter
36+
{
37+
public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
38+
new ValueTask<int>(stream.Read(buffer.Span));
39+
40+
public static ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) =>
41+
new ValueTask<int>(stream.ReadAtLeast(buffer.Span, minimumBytes, throwOnEndOfStream));
42+
43+
public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
44+
{
45+
stream.Write(buffer.Span);
46+
return default;
47+
}
48+
49+
public static Task FlushAsync(Stream stream, CancellationToken cancellationToken)
50+
{
51+
stream.Flush();
52+
return Task.CompletedTask;
53+
}
54+
55+
public static Task WaitAsync(TaskCompletionSource<bool> waiter)
56+
{
57+
waiter.Task.GetAwaiter().GetResult();
58+
return Task.CompletedTask;
59+
}
60+
}
61+
}

src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ private void SendMessageCallback(IAsyncResult result)
892892
{
893893
try
894894
{
895-
_message!.EndSend(result);
895+
MailMessage.EndSend(result);
896896
// If some recipients failed but not others, throw AFTER sending the message.
897897
Complete(_failedRecipientException, result.AsyncState!);
898898
}
@@ -929,8 +929,7 @@ private void SendMailCallback(IAsyncResult result)
929929
}
930930
else
931931
{
932-
_message!.BeginSend(_writer,
933-
IsUnicodeSupported(), new AsyncCallback(SendMessageCallback), result.AsyncState!);
932+
_message!.BeginSend(_writer, DeliveryMethod != SmtpDeliveryMethod.Network, IsUnicodeSupported(), new AsyncCallback(SendMessageCallback), result.AsyncState!);
934933
}
935934
}
936935
catch (Exception e)

0 commit comments

Comments
 (0)