Skip to content

Commit cdb80ff

Browse files
liveansMihaZupan
andauthored
Adding HttpWebRequest to HttpClient Migration Guide (#42242)
* Adding new file and basic start * Add complex examples * Add HttpWebRequest API Surface Mapping * Change header styles * Try link * Adding more xrefs * Add more examples and started to design sections * More sections * Fix broken link * Fix some todos and add new sections * More todo fix * Review feedbacks * Fix broken links * Fix more todos * Add examples * Fix broken links * Add DnsRoundRobin to build * More todo fix * Add warning about httpwebrequest * Last todo fix and a bit refactoring * Fix warnings * Review feedback - handle exception and dispose socket on ConnectCallback * Apply suggestions from code review Co-authored-by: Miha Zupan <[email protected]> * Review feedback * Review feedback * Fix links * Link to raw gh page --------- Co-authored-by: Miha Zupan <[email protected]>
1 parent 5b18482 commit cdb80ff

File tree

7 files changed

+1129
-0
lines changed

7 files changed

+1129
-0
lines changed

docs/fundamentals/networking/http/httpclient-migrate-from-httpwebrequest.md

Lines changed: 625 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Collections.Concurrent;
2+
using System.Diagnostics;
3+
using System.Net;
4+
using System.Net.Cache;
5+
using System.Net.Http.Headers;
6+
using System.Net.Sockets;
7+
8+
// <CachePolicy>
9+
public static class CachePolicy
10+
{
11+
public static void AddCacheControlHeaders(HttpRequestMessage request, RequestCachePolicy policy)
12+
// </CachePolicy>
13+
{
14+
if (policy != null && policy.Level != RequestCacheLevel.BypassCache)
15+
{
16+
CacheControlHeaderValue? cacheControl = null;
17+
HttpHeaderValueCollection<NameValueHeaderValue> pragmaHeaders = request.Headers.Pragma;
18+
19+
if (policy is HttpRequestCachePolicy httpRequestCachePolicy)
20+
{
21+
switch (httpRequestCachePolicy.Level)
22+
{
23+
case HttpRequestCacheLevel.NoCacheNoStore:
24+
cacheControl = new CacheControlHeaderValue
25+
{
26+
NoCache = true,
27+
NoStore = true
28+
};
29+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
30+
break;
31+
case HttpRequestCacheLevel.Reload:
32+
cacheControl = new CacheControlHeaderValue
33+
{
34+
NoCache = true
35+
};
36+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
37+
break;
38+
case HttpRequestCacheLevel.CacheOnly:
39+
throw new WebException("CacheOnly is not supported!");
40+
case HttpRequestCacheLevel.CacheOrNextCacheOnly:
41+
cacheControl = new CacheControlHeaderValue
42+
{
43+
OnlyIfCached = true
44+
};
45+
break;
46+
case HttpRequestCacheLevel.Default:
47+
cacheControl = new CacheControlHeaderValue();
48+
49+
if (httpRequestCachePolicy.MinFresh > TimeSpan.Zero)
50+
{
51+
cacheControl.MinFresh = httpRequestCachePolicy.MinFresh;
52+
}
53+
54+
if (httpRequestCachePolicy.MaxAge != TimeSpan.MaxValue)
55+
{
56+
cacheControl.MaxAge = httpRequestCachePolicy.MaxAge;
57+
}
58+
59+
if (httpRequestCachePolicy.MaxStale > TimeSpan.Zero)
60+
{
61+
cacheControl.MaxStale = true;
62+
cacheControl.MaxStaleLimit = httpRequestCachePolicy.MaxStale;
63+
}
64+
65+
break;
66+
case HttpRequestCacheLevel.Refresh:
67+
cacheControl = new CacheControlHeaderValue
68+
{
69+
MaxAge = TimeSpan.Zero
70+
};
71+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
72+
break;
73+
}
74+
}
75+
else
76+
{
77+
switch (policy.Level)
78+
{
79+
case RequestCacheLevel.NoCacheNoStore:
80+
cacheControl = new CacheControlHeaderValue
81+
{
82+
NoCache = true,
83+
NoStore = true
84+
};
85+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
86+
break;
87+
case RequestCacheLevel.Reload:
88+
cacheControl = new CacheControlHeaderValue
89+
{
90+
NoCache = true
91+
};
92+
pragmaHeaders.Add(new NameValueHeaderValue("no-cache"));
93+
break;
94+
case RequestCacheLevel.CacheOnly:
95+
throw new Exception("CacheOnly is not supported!");
96+
}
97+
}
98+
99+
if (cacheControl != null)
100+
{
101+
request.Headers.CacheControl = cacheControl;
102+
}
103+
}
104+
}
105+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using System.Collections.Concurrent;
2+
using System.Diagnostics;
3+
using System.Net;
4+
using System.Net.Sockets;
5+
6+
// <DnsRoundRobinConnector>
7+
// This is available as NuGet Package: https://www.nuget.org/packages/DnsRoundRobin/
8+
// The original source code can be found also here: https://github.com/MihaZupan/DnsRoundRobin
9+
public sealed class DnsRoundRobinConnector : IDisposable
10+
// </DnsRoundRobinConnector>
11+
{
12+
private const int DefaultDnsRefreshIntervalSeconds = 2 * 60;
13+
private const int MaxCleanupIntervalSeconds = 60;
14+
15+
public static DnsRoundRobinConnector Shared { get; } = new();
16+
17+
private readonly ConcurrentDictionary<string, HostRoundRobinState> _states = new(StringComparer.Ordinal);
18+
private readonly Timer _cleanupTimer;
19+
private readonly TimeSpan _cleanupInterval;
20+
private readonly long _cleanupIntervalTicks;
21+
private readonly long _dnsRefreshTimeoutTicks;
22+
private readonly TimeSpan _endpointConnectTimeout;
23+
24+
/// <summary>
25+
/// Creates a new <see cref="DnsRoundRobinConnector"/>.
26+
/// </summary>
27+
/// <param name="dnsRefreshInterval">Maximum amount of time a Dns resolution is cached for. Default to 2 minutes.</param>
28+
/// <param name="endpointConnectTimeout">Maximum amount of time allowed for a connection attempt to any individual endpoint. Defaults to infinite.</param>
29+
public DnsRoundRobinConnector(TimeSpan? dnsRefreshInterval = null, TimeSpan? endpointConnectTimeout = null)
30+
{
31+
dnsRefreshInterval = TimeSpan.FromSeconds(Math.Max(1, dnsRefreshInterval?.TotalSeconds ?? DefaultDnsRefreshIntervalSeconds));
32+
_cleanupInterval = TimeSpan.FromSeconds(Math.Clamp(dnsRefreshInterval.Value.TotalSeconds / 2, 1, MaxCleanupIntervalSeconds));
33+
_cleanupIntervalTicks = (long)(_cleanupInterval.TotalSeconds * Stopwatch.Frequency);
34+
_dnsRefreshTimeoutTicks = (long)(dnsRefreshInterval.Value.TotalSeconds * Stopwatch.Frequency);
35+
_endpointConnectTimeout = endpointConnectTimeout is null || endpointConnectTimeout.Value.Ticks < 1 ? Timeout.InfiniteTimeSpan : endpointConnectTimeout.Value;
36+
37+
bool restoreFlow = false;
38+
try
39+
{
40+
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever
41+
if (!ExecutionContext.IsFlowSuppressed())
42+
{
43+
ExecutionContext.SuppressFlow();
44+
restoreFlow = true;
45+
}
46+
47+
// Ensure the Timer has a weak reference to the connector; otherwise, it
48+
// can introduce a cycle that keeps the connector rooted by the Timer
49+
_cleanupTimer = new Timer(static state =>
50+
{
51+
var thisWeakRef = (WeakReference<DnsRoundRobinConnector>)state!;
52+
if (thisWeakRef.TryGetTarget(out DnsRoundRobinConnector? thisRef))
53+
{
54+
thisRef.Cleanup();
55+
thisRef._cleanupTimer.Change(thisRef._cleanupInterval, Timeout.InfiniteTimeSpan);
56+
}
57+
}, new WeakReference<DnsRoundRobinConnector>(this), Timeout.Infinite, Timeout.Infinite);
58+
59+
_cleanupTimer.Change(_cleanupInterval, Timeout.InfiniteTimeSpan);
60+
}
61+
finally
62+
{
63+
if (restoreFlow)
64+
{
65+
ExecutionContext.RestoreFlow();
66+
}
67+
}
68+
}
69+
70+
private void Cleanup()
71+
{
72+
long minTimestamp = Stopwatch.GetTimestamp() - _cleanupIntervalTicks;
73+
74+
foreach (KeyValuePair<string, HostRoundRobinState> state in _states)
75+
{
76+
if (state.Value.LastAccessTimestamp < minTimestamp)
77+
{
78+
_states.TryRemove(state);
79+
}
80+
}
81+
}
82+
83+
public void Dispose()
84+
{
85+
_states.Clear();
86+
}
87+
88+
public Task<Socket> ConnectAsync(DnsEndPoint endPoint, CancellationToken cancellationToken)
89+
{
90+
if (cancellationToken.IsCancellationRequested)
91+
{
92+
return Task.FromCanceled<Socket>(cancellationToken);
93+
}
94+
95+
if (IPAddress.TryParse(endPoint.Host, out IPAddress? address))
96+
{
97+
// Avoid the overhead of HostRoundRobinState if we're dealing with a single endpoint
98+
return ConnectToIPAddressAsync(address, endPoint.Port, cancellationToken);
99+
}
100+
101+
HostRoundRobinState state = _states.GetOrAdd(
102+
endPoint.Host,
103+
static (_, thisRef) => new HostRoundRobinState(thisRef._dnsRefreshTimeoutTicks, thisRef._endpointConnectTimeout),
104+
this);
105+
106+
return state.ConnectAsync(endPoint, cancellationToken);
107+
}
108+
109+
private static async Task<Socket> ConnectToIPAddressAsync(IPAddress address, int port, CancellationToken cancellationToken)
110+
{
111+
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
112+
try
113+
{
114+
await socket.ConnectAsync(address, port, cancellationToken);
115+
return socket;
116+
}
117+
catch
118+
{
119+
socket.Dispose();
120+
throw;
121+
}
122+
}
123+
124+
private sealed class HostRoundRobinState
125+
{
126+
private readonly long _dnsRefreshTimeoutTicks;
127+
private readonly TimeSpan _endpointConnectTimeout;
128+
private long _lastAccessTimestamp;
129+
private long _lastDnsTimestamp;
130+
private IPAddress[]? _addresses;
131+
private uint _roundRobinIndex;
132+
133+
public long LastAccessTimestamp => Volatile.Read(ref _lastAccessTimestamp);
134+
135+
private bool AddressesAreStale => Stopwatch.GetTimestamp() - Volatile.Read(ref _lastDnsTimestamp) > _dnsRefreshTimeoutTicks;
136+
137+
public HostRoundRobinState(long dnsRefreshTimeoutTicks, TimeSpan endpointConnectTimeout)
138+
{
139+
_dnsRefreshTimeoutTicks = dnsRefreshTimeoutTicks;
140+
_endpointConnectTimeout = endpointConnectTimeout;
141+
142+
_roundRobinIndex--; // Offset the first Increment to ensure we start with the first address in the list
143+
144+
RefreshLastAccessTimestamp();
145+
}
146+
147+
private void RefreshLastAccessTimestamp() => Volatile.Write(ref _lastAccessTimestamp, Stopwatch.GetTimestamp());
148+
149+
public async Task<Socket> ConnectAsync(DnsEndPoint endPoint, CancellationToken cancellationToken)
150+
{
151+
RefreshLastAccessTimestamp();
152+
153+
uint sharedIndex = Interlocked.Increment(ref _roundRobinIndex);
154+
IPAddress[]? attemptedAddresses = null;
155+
IPAddress[]? addresses = null;
156+
Exception? lastException = null;
157+
158+
while (attemptedAddresses is null)
159+
{
160+
if (addresses is null)
161+
{
162+
addresses = _addresses;
163+
}
164+
else
165+
{
166+
attemptedAddresses = addresses;
167+
168+
// Give each connection attempt a chance to do its own Dns call.
169+
addresses = null;
170+
}
171+
172+
if (addresses is null || AddressesAreStale)
173+
{
174+
// It's possible that multiple connection attempts are resolving the same host concurrently - that's okay.
175+
_addresses = addresses = await Dns.GetHostAddressesAsync(endPoint.Host, cancellationToken);
176+
Volatile.Write(ref _lastDnsTimestamp, Stopwatch.GetTimestamp());
177+
178+
if (attemptedAddresses is not null && AddressListsAreEquivalent(attemptedAddresses, addresses))
179+
{
180+
// We've already tried to connect to every address in the list, and a new Dns resolution returned the same list.
181+
// Instead of attempting every address again, give up early.
182+
break;
183+
}
184+
}
185+
186+
for (int i = 0; i < addresses.Length; i++)
187+
{
188+
Socket? attemptSocket = null;
189+
CancellationTokenSource? endpointConnectTimeoutCts = null;
190+
try
191+
{
192+
IPAddress address = addresses[(int)((sharedIndex + i) % addresses.Length)];
193+
194+
if (Socket.OSSupportsIPv6 && address.AddressFamily == AddressFamily.InterNetworkV6)
195+
{
196+
attemptSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
197+
if (address.IsIPv4MappedToIPv6)
198+
{
199+
attemptSocket.DualMode = true;
200+
}
201+
}
202+
else if (Socket.OSSupportsIPv4 && address.AddressFamily == AddressFamily.InterNetwork)
203+
{
204+
attemptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
205+
}
206+
207+
if (attemptSocket is not null)
208+
{
209+
attemptSocket.NoDelay = true;
210+
211+
if (_endpointConnectTimeout != Timeout.InfiniteTimeSpan)
212+
{
213+
endpointConnectTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
214+
endpointConnectTimeoutCts.CancelAfter(_endpointConnectTimeout);
215+
}
216+
217+
await attemptSocket.ConnectAsync(address, endPoint.Port, endpointConnectTimeoutCts?.Token ?? cancellationToken);
218+
219+
RefreshLastAccessTimestamp();
220+
return attemptSocket;
221+
}
222+
}
223+
catch (Exception ex)
224+
{
225+
attemptSocket?.Dispose();
226+
227+
if (cancellationToken.IsCancellationRequested)
228+
{
229+
throw;
230+
}
231+
232+
if (endpointConnectTimeoutCts?.IsCancellationRequested == true)
233+
{
234+
ex = new TimeoutException($"Failed to connect to any endpoint within the specified endpoint connect timeout of {_endpointConnectTimeout.TotalSeconds:N2} seconds.", ex);
235+
}
236+
237+
lastException = ex;
238+
}
239+
finally
240+
{
241+
endpointConnectTimeoutCts?.Dispose();
242+
}
243+
}
244+
}
245+
246+
throw lastException ?? new SocketException((int)SocketError.NoData);
247+
}
248+
249+
private static bool AddressListsAreEquivalent(IPAddress[] left, IPAddress[] right)
250+
{
251+
if (ReferenceEquals(left, right))
252+
{
253+
return true;
254+
}
255+
256+
if (left.Length != right.Length)
257+
{
258+
return false;
259+
}
260+
261+
for (int i = 0; i < left.Length; i++)
262+
{
263+
if (!left[i].Equals(right[i]))
264+
{
265+
return false;
266+
}
267+
}
268+
269+
return true;
270+
}
271+
}
272+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Net;
2+
using System.Net.Cache;
3+
using System.Net.Http;
4+
using System.Net.Sockets;
5+
6+
static partial class Program
7+
{
8+
// <CacheControlProgram>
9+
static async Task AddCacheControlHeaders()
10+
{
11+
HttpClient client = new HttpClient();
12+
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, Uri);
13+
CachePolicy.AddCacheControlHeaders(requestMessage, new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore));
14+
HttpResponseMessage response = await client.SendAsync(requestMessage);
15+
}
16+
// </CacheControlProgram>
17+
}

0 commit comments

Comments
 (0)