diff --git a/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj b/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj
index bafb556b55d3..ded7faa4d510 100644
--- a/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj
+++ b/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj
@@ -4,7 +4,9 @@
true
-
+
+
+
diff --git a/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs b/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs
index a71ca1ac8a2f..e6f7e3f61899 100644
--- a/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs
+++ b/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs
@@ -21,6 +21,8 @@ public class MockTransport : HttpPipelineTransport
public bool? ExpectSyncPipeline { get; set; }
+ public List TransportUpdates { get; } = [];
+
public MockTransport()
{
RequestGate = new AsyncGate();
@@ -38,7 +40,7 @@ public MockTransport(params MockResponse[] responses)
};
}
- public MockTransport(Func responseFactory): this(req => responseFactory((MockRequest)req.Request))
+ public MockTransport(Func responseFactory) : this(req => responseFactory((MockRequest)req.Request))
{
}
@@ -72,6 +74,11 @@ public override async ValueTask ProcessAsync(HttpMessage message)
await ProcessCore(message);
}
+ public override void Update(HttpPipelineTransportOptions options)
+ {
+ TransportUpdates.Add(options);
+ }
+
private async Task ProcessCore(HttpMessage message)
{
if (!(message.Request is MockRequest request))
diff --git a/sdk/core/Azure.Core/api/Azure.Core.net462.cs b/sdk/core/Azure.Core/api/Azure.Core.net462.cs
index 8136f6d21825..c9a2bb5e336d 100644
--- a/sdk/core/Azure.Core/api/Azure.Core.net462.cs
+++ b/sdk/core/Azure.Core/api/Azure.Core.net462.cs
@@ -306,6 +306,8 @@ public partial struct AccessToken
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
+ public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
+ public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
public System.DateTimeOffset ExpiresOn { get { throw null; } }
public System.DateTimeOffset? RefreshOn { get { throw null; } }
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
{
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
public HttpClientTransport() { }
+ public HttpClientTransport(System.Func clientFactory) { }
+ public HttpClientTransport(System.Func handlerFactory) { }
public HttpClientTransport(System.Net.Http.HttpClient client) { }
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
public sealed override Azure.Core.Request CreateRequest() { throw null; }
public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
+ public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipeline
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
public abstract Azure.Core.Request CreateRequest();
public abstract void Process(Azure.Core.HttpMessage message);
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
+ public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipelineTransportOptions
{
diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs
index 8136f6d21825..c9a2bb5e336d 100644
--- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs
+++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs
@@ -306,6 +306,8 @@ public partial struct AccessToken
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
+ public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
+ public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
public System.DateTimeOffset ExpiresOn { get { throw null; } }
public System.DateTimeOffset? RefreshOn { get { throw null; } }
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
{
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
public HttpClientTransport() { }
+ public HttpClientTransport(System.Func clientFactory) { }
+ public HttpClientTransport(System.Func handlerFactory) { }
public HttpClientTransport(System.Net.Http.HttpClient client) { }
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
public sealed override Azure.Core.Request CreateRequest() { throw null; }
public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
+ public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipeline
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
public abstract Azure.Core.Request CreateRequest();
public abstract void Process(Azure.Core.HttpMessage message);
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
+ public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipelineTransportOptions
{
diff --git a/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs
index 3eb502afc9ff..44dddb16c14d 100644
--- a/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs
+++ b/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs
@@ -315,6 +315,8 @@ public partial struct AccessToken
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
+ public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
+ public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
public System.DateTimeOffset ExpiresOn { get { throw null; } }
public System.DateTimeOffset? RefreshOn { get { throw null; } }
public string Token { get { throw null; } }
@@ -1055,12 +1057,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
{
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
public HttpClientTransport() { }
+ public HttpClientTransport(System.Func clientFactory) { }
+ public HttpClientTransport(System.Func handlerFactory) { }
public HttpClientTransport(System.Net.Http.HttpClient client) { }
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
public sealed override Azure.Core.Request CreateRequest() { throw null; }
public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
+ public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipeline
{
@@ -1117,6 +1122,7 @@ protected HttpPipelineTransport() { }
public abstract Azure.Core.Request CreateRequest();
public abstract void Process(Azure.Core.HttpMessage message);
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
+ public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipelineTransportOptions
{
diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
index 8136f6d21825..c9a2bb5e336d 100644
--- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
+++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
@@ -306,6 +306,8 @@ public partial struct AccessToken
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
+ public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
+ public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
public System.DateTimeOffset ExpiresOn { get { throw null; } }
public System.DateTimeOffset? RefreshOn { get { throw null; } }
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
{
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
public HttpClientTransport() { }
+ public HttpClientTransport(System.Func clientFactory) { }
+ public HttpClientTransport(System.Func handlerFactory) { }
public HttpClientTransport(System.Net.Http.HttpClient client) { }
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
public sealed override Azure.Core.Request CreateRequest() { throw null; }
public void Dispose() { }
public override void Process(Azure.Core.HttpMessage message) { }
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
+ public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipeline
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
public abstract Azure.Core.Request CreateRequest();
public abstract void Process(Azure.Core.HttpMessage message);
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
+ public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
}
public partial class HttpPipelineTransportOptions
{
diff --git a/sdk/core/Azure.Core/src/AccessToken.cs b/sdk/core/Azure.Core/src/AccessToken.cs
index a23b64964119..0cc3d8d63028 100644
--- a/sdk/core/Azure.Core/src/AccessToken.cs
+++ b/sdk/core/Azure.Core/src/AccessToken.cs
@@ -3,6 +3,7 @@
using System;
using System.ClientModel.Primitives;
+using System.Security.Cryptography.X509Certificates;
namespace Azure.Core
{
@@ -52,6 +53,23 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset?
TokenType = tokenType;
}
+ ///
+ /// Creates a new instance of using the provided and .
+ ///
+ /// The access token value.
+ /// The access token expiry date.
+ /// Specifies the time when the cached token should be proactively refreshed.
+ /// The access token type.
+ /// The binding certificate for the access token.
+ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset? refreshOn, string tokenType, X509Certificate2 bindingCertificate)
+ {
+ Token = accessToken;
+ ExpiresOn = expiresOn;
+ RefreshOn = refreshOn;
+ TokenType = tokenType;
+ BindingCertificate = bindingCertificate;
+ }
+
///
/// Get the access token value.
///
@@ -72,6 +90,13 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset?
///
public string TokenType { get; }
+ ///
+ /// Gets or sets the binding certificate for the access token.
+ /// This is used when authenticating via Proof of Possession (PoP).
+ ///
+ ///
+ public X509Certificate2? BindingCertificate { get; }
+
///
public override bool Equals(object? obj)
{
diff --git a/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs b/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs
index b65bbb1ccf05..e0c15f4854b2 100644
--- a/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs
+++ b/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs
@@ -36,6 +36,7 @@ internal sealed class AzureCoreEventSource : AzureEventSource
private const int RequestRedirectCountExceededEvent = 22;
private const int PipelineTransportOptionsNotAppliedEvent = 23;
private const int FailedToDecodeCaeChallengeClaimsEvent = 24;
+ private const int FailedToUpdateTransportEvent = 25;
private AzureCoreEventSource() : base(EventSourceName) { }
@@ -333,5 +334,14 @@ public void FailedToDecodeCaeChallengeClaims(string? encodedClaims, string excep
{
WriteEvent(FailedToDecodeCaeChallengeClaimsEvent, encodedClaims, exception);
}
+
+ [Event(FailedToUpdateTransportEvent, Level = EventLevel.Error, Message = "Failed to update transport: {0}")]
+ public void FailedToUpdateTransport(string reason)
+ {
+ if (IsEnabled(EventLevel.Error, EventKeywords.None))
+ {
+ WriteEvent(FailedToUpdateTransportEvent, reason);
+ }
+ }
}
}
diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs
index 07f21a103c9a..a3ee696c596a 100644
--- a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs
+++ b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs
@@ -10,6 +10,7 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
+using Azure.Core.Diagnostics;
namespace Azure.Core.Pipeline
{
@@ -18,25 +19,86 @@ namespace Azure.Core.Pipeline
///
public partial class HttpClientTransport : HttpPipelineTransport, IDisposable
{
+ ///
+ /// Reference-counted wrapper around HttpClient to ensure safe disposal during concurrent access.
+ ///
+ private sealed class HttpClientWrapper
+ {
+ private readonly HttpClient _client;
+ private volatile int _refCount = 1; // Start with 1 reference (the transport itself)
+
+ public HttpClientWrapper(HttpClient client, bool disableRefCounting = false)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ IsRefCountingEnabled = !disableRefCounting;
+ }
+
+ public HttpClient Client => _client;
+
+ public bool IsRefCountingEnabled { get; set; }
+
+ ///
+ /// Atomically increment reference count if not disposed.
+ ///
+ /// True if reference was successfully added, false if already disposed.
+ public bool TryAddRef()
+ {
+ int currentCount;
+ do
+ {
+ currentCount = _refCount;
+ if (currentCount == 0) return false; // Already disposed
+ }
+ while (Interlocked.CompareExchange(ref _refCount, currentCount + 1, currentCount) != currentCount);
+
+ return true;
+ }
+
+ ///
+ /// Atomically decrement reference count and dispose if needed.
+ ///
+ public void Release()
+ {
+ if (Interlocked.Decrement(ref _refCount) == 0)
+ {
+ _client.Dispose();
+ }
+ }
+ }
+
///
/// A shared instance of with default parameters.
///
public static readonly HttpClientTransport Shared = new HttpClientTransport();
+ private volatile HttpClientWrapper _clientWrapper;
+
// The transport's private HttpClient is internal because it is used by tests.
- internal HttpClient Client { get; }
+ internal HttpClient Client => _clientWrapper.Client ?? throw new ObjectDisposedException(nameof(HttpClientTransport));
+
+ internal Func? _clientFactory { get; }
+ internal Func? _handlerFactory { get; }
+
+ ///
+ /// Internal property for testing: indicates whether reference counting is enabled for this transport instance.
+ ///
+ internal bool IsRefCountingEnabled => _clientWrapper?.IsRefCountingEnabled ?? false;
///
/// Creates a new instance using default configuration.
///
public HttpClientTransport() : this(CreateDefaultClient())
- { }
+ {
+ _clientFactory = CreateDefaultClient;
+ _clientWrapper.IsRefCountingEnabled = true;
+ }
///
/// Creates a new instance using default configuration.
///
/// The that to configure the behavior of the transport.
- internal HttpClientTransport(HttpPipelineTransportOptions? options = null) : this(CreateDefaultClient(options))
+ internal HttpClientTransport(HttpPipelineTransportOptions? options = null)
+ : this(_ => CreateDefaultClient(options))
{ }
///
@@ -45,7 +107,19 @@ internal HttpClientTransport(HttpPipelineTransportOptions? options = null) : thi
/// The instance of to use.
public HttpClientTransport(HttpMessageHandler messageHandler)
{
- Client = new HttpClient(messageHandler) ?? throw new ArgumentNullException(nameof(messageHandler));
+ var client = new HttpClient(messageHandler ?? throw new ArgumentNullException(nameof(messageHandler)));
+ _clientWrapper = new HttpClientWrapper(client, true);
+ }
+
+ ///
+ /// Creates a new instance of using the provided handler factory.
+ ///
+ /// The factory function to create the message handler.
+ public HttpClientTransport(Func handlerFactory)
+ : this(handlerFactory.Invoke(new HttpPipelineTransportOptions()))
+ {
+ _handlerFactory = handlerFactory;
+ _clientWrapper.IsRefCountingEnabled = true;
}
///
@@ -54,7 +128,55 @@ public HttpClientTransport(HttpMessageHandler messageHandler)
/// The instance of to use.
public HttpClientTransport(HttpClient client)
{
- Client = client ?? throw new ArgumentNullException(nameof(client));
+ _clientWrapper = new HttpClientWrapper(client ?? throw new ArgumentNullException(nameof(client)), true);
+ }
+
+ ///
+ /// Creates a new instance of using the provided factory.
+ ///
+ /// The factory function to create the client.
+ public HttpClientTransport(Func clientFactory) : this(clientFactory.Invoke(new HttpPipelineTransportOptions()))
+ {
+ _clientFactory = clientFactory;
+ _clientWrapper.IsRefCountingEnabled = true;
+ }
+
+ ///
+ public override void Update(HttpPipelineTransportOptions options)
+ {
+ if (this == Shared)
+ {
+ throw new InvalidOperationException("Cannot update the shared HttpClientTransport instance.");
+ }
+
+ HttpClient? newClient = null;
+ if (_clientFactory != null && _clientFactory != CreateDefaultClient)
+ {
+ newClient = _clientFactory(options);
+ }
+ else if (_handlerFactory != null)
+ {
+ var handler = _handlerFactory(options);
+ newClient = new HttpClient(handler);
+ }
+ else
+ {
+ // No factory to create a new client, so we cannot update.
+ if (_clientFactory != CreateDefaultClient)
+ {
+ AzureCoreEventSource.Singleton.FailedToUpdateTransport("No factory available to create a new HttpClient instance.");
+ } else
+ {
+ AzureCoreEventSource.Singleton.FailedToUpdateTransport("Skipping transport update because no custom factory is available to create a new HttpClient instance.");
+ }
+ return;
+ }
+
+ var newWrapper = new HttpClientWrapper(newClient);
+ var oldWrapper = Interlocked.Exchange(ref _clientWrapper!, newWrapper);
+
+ // Release the transport's reference to the old client
+ oldWrapper?.Release();
}
///
@@ -86,8 +208,17 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async)
HttpResponseMessage responseMessage;
Stream? contentStream = null;
message.ClearResponse();
+
+ // Get reference-counted access to the client
+ var clientWrapper = _clientWrapper;
+ if (clientWrapper.IsRefCountingEnabled && !clientWrapper.TryAddRef())
+ {
+ throw new ObjectDisposedException(nameof(HttpClientTransport));
+ }
+
try
{
+ var localClient = clientWrapper.Client;
#if NET5_0_OR_GREATER
if (!async)
{
@@ -95,14 +226,14 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async)
// HttpClient.Send would throw a NotSupported exception instead of GetAwaiter().GetResult()
// throwing a System.Threading.SynchronizationLockException: Cannot wait on monitors on this runtime.
#pragma warning disable CA1416 // 'HttpClient.Send(HttpRequestMessage, HttpCompletionOption, CancellationToken)' is unsupported on 'browser'
- responseMessage = Client.Send(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken);
+ responseMessage = localClient.Send(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken);
#pragma warning restore CA1416
}
else
#endif
{
#pragma warning disable AZC0110 // DO NOT use await keyword in possibly synchronous scope.
- responseMessage = await Client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken)
+ responseMessage = await localClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken)
#pragma warning restore AZC0110 // DO NOT use await keyword in possibly synchronous scope.
.ConfigureAwait(false);
}
@@ -134,6 +265,14 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async)
{
throw new RequestFailedException(e.Message, e);
}
+ finally
+ {
+ if (clientWrapper.IsRefCountingEnabled)
+ {
+ // Release the reference to the client wrapper
+ clientWrapper.Release();
+ }
+ }
message.Response = new HttpClientTransportResponse(message.Request.ClientRequestId, responseMessage, contentStream);
}
@@ -253,7 +392,7 @@ public void Dispose()
{
if (this != Shared)
{
- Client.Dispose();
+ _clientWrapper?.Release();
}
GC.SuppressFinalize(this);
diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs
index e0c42f79fd5a..acebf2fcbf9a 100644
--- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs
+++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs
@@ -58,6 +58,16 @@ public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[]? polic
ClientDiagnostics.CreateMessageSanitizer(new DiagnosticsOptions()));
policies.CopyTo(all, 0);
+ for (int i = 0; i < policies.Length; i++)
+ {
+ // If the policy implements ISupportsTransportCertificateUpdate, we need to subscribe to its TransportUpdated event
+ if (policies[i] is ISupportsTransportUpdate transportUpdated)
+ {
+ transportUpdated.TransportOptionsChanged += options => _transport.Update(options);
+ break;
+ }
+ }
+
_pipeline = all;
}
@@ -78,6 +88,16 @@ internal HttpPipeline(
_perCallIndex = perCallIndex;
_perRetryIndex = perRetryIndex;
_internallyConstructed = true;
+
+ for (int i = 0; i < pipeline.Length; i++)
+ {
+ // If the policy implements ISupportsTransportCertificateUpdate, we need to subscribe to its TransportUpdated event
+ if (pipeline[i] is ISupportsTransportUpdate transportUpdated)
+ {
+ transportUpdated.TransportOptionsChanged += options => _transport.Update(options);
+ break;
+ }
+ }
}
///
diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs
index a9daa7f45007..99c8dd456026 100644
--- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs
+++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -30,6 +32,16 @@ public abstract class HttpPipelineTransport
///
public abstract Request CreateRequest();
+ ///
+ /// Updates the transport with the provided .
+ ///
+ /// The options to use for updating the transport.
+
+ public virtual void Update(HttpPipelineTransportOptions options)
+ {
+ throw new NotSupportedException("This transport does not support updating options.");
+ }
+
///
/// Creates the default based on the current environment and configuration.
///
diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs
index 3d16d4171f6f..ad558d5fd925 100644
--- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs
+++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs
@@ -34,12 +34,33 @@ public HttpPipelineTransportOptions()
///
public IList ClientCertificates { get; }
- ///
+ ///
/// Gets or sets a value that indicates whether the redirect policy should follow redirection responses.
///
///
/// true if the redirect policy should follow redirection responses; otherwise false. The default value is false.
///
public bool IsClientRedirectEnabled { get; set; }
+
+ ///
+ /// Creates a clone of the current instance with the same settings.
+ ///
+ ///
+ ///
+ /// A new instance with the same settings as the current instance.
+ ///
+ internal HttpPipelineTransportOptions Clone()
+ {
+ var clone = new HttpPipelineTransportOptions
+ {
+ ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback,
+ IsClientRedirectEnabled = IsClientRedirectEnabled,
+ };
+ foreach (X509Certificate2 certificate in ClientCertificates)
+ {
+ clone.ClientCertificates.Add(certificate);
+ }
+ return clone;
+ }
}
}
diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs
index 3b69d4529cfe..3f66f9cbd84f 100644
--- a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs
+++ b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs
@@ -3,6 +3,7 @@
using System;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
namespace Azure.Core.Pipeline
@@ -13,9 +14,10 @@ namespace Azure.Core.Pipeline
///
internal partial class HttpWebRequestTransport : HttpPipelineTransport
{
- private readonly Action _configureRequest;
+ internal volatile Action _configureRequest;
public static readonly HttpWebRequestTransport Shared = new HttpWebRequestTransport();
private readonly IWebProxy? _environmentProxy;
+ internal Func? TransportFactory { get; set; }
///
/// Creates a new instance of
@@ -37,6 +39,19 @@ internal HttpWebRequestTransport(Action configureRequest)
}
}
+ ///
+ public override void Update(HttpPipelineTransportOptions options)
+ {
+ if (this == Shared)
+ {
+ throw new InvalidOperationException("Cannot update the shared HttpWebRequestTransport instance.");
+ }
+
+ Action newConfigureRequest = req => ApplyOptionsToRequest(req, options);
+
+ Interlocked.Exchange(ref _configureRequest, newConfigureRequest);
+ }
+
///
public override void Process(HttpMessage message)
{
diff --git a/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs b/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs
new file mode 100644
index 000000000000..19087b502d29
--- /dev/null
+++ b/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Azure.Core.Pipeline;
+
+///
+/// Indicates that the transport supports updating its transport options.
+///
+internal interface ISupportsTransportUpdate
+{
+ ///
+ /// Event that is triggered when the transport needs to be updated.
+ ///
+ public event Action? TransportOptionsChanged;
+}
\ No newline at end of file
diff --git a/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs b/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs
index 89e951496ae1..7aee3b5c5c71 100644
--- a/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs
+++ b/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs
@@ -22,7 +22,6 @@ namespace Azure.Core.Tests
[NonParallelizable]
public class HttpClientTransportFunctionalTest : TransportFunctionalTests
{
- private static RemoteCertificateValidationCallback certCallback = (_, _, _, _) => true;
public HttpClientTransportFunctionalTest(bool isAsync) : base(isAsync)
{ }
@@ -78,7 +77,7 @@ public void UsesHttpClientTransportOnNetStandart()
[Test]
public void DisposeDisposesClient()
{
- var transport = (HttpClientTransport)GetTransport(options:new HttpPipelineTransportOptions());
+ var transport = (HttpClientTransport)GetTransport(options: new HttpPipelineTransportOptions());
transport.Dispose();
Assert.Throws(() => transport.Client.CancelPendingRequests());
}
diff --git a/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs b/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs
new file mode 100644
index 000000000000..b3b7a7d4f67a
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs
@@ -0,0 +1,357 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.Core.Pipeline;
+using Azure.Core.TestFramework;
+using NUnit.Framework;
+
+namespace Azure.Core.Tests
+{
+ [TestFixture(true)]
+ [TestFixture(false)]
+ public class HttpClientTransportReferenceCountingTests : SyncAsyncTestBase
+ {
+ public HttpClientTransportReferenceCountingTests(bool isAsync) : base(isAsync)
+ {
+ }
+
+ [Test]
+ public void HttpClientConstructor_CannotBeUpdated_IndicatesNoReferenceCountingNeeded()
+ {
+ // Arrange
+ using var client = new HttpClient();
+
+ // Act
+ using var transport = new HttpClientTransport(client);
+
+ // Assert - Reference counting should be disabled for direct HttpClient constructor
+ Assert.IsFalse(transport.IsRefCountingEnabled,
+ "Reference counting should be disabled when using direct HttpClient constructor");
+
+ // Update should not throw, but should be a no-op
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public void HttpMessageHandlerConstructor_CannotBeUpdated_IndicatesNoReferenceCountingNeeded()
+ {
+ // Arrange
+ using var handler = new HttpClientHandler();
+
+ // Act
+ using var transport = new HttpClientTransport(handler);
+
+ // Assert - Reference counting should be disabled for direct HttpMessageHandler constructor
+ Assert.IsFalse(transport.IsRefCountingEnabled,
+ "Reference counting should be disabled when using direct HttpMessageHandler constructor");
+
+ // Update should not throw, but should be a no-op
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public void ClientFactoryConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled()
+ {
+ // Arrange
+ Func clientFactory = _ => new HttpClient();
+
+ // Act
+ using var transport = new HttpClientTransport(clientFactory);
+
+ // Assert - Reference counting should be enabled and updates should work
+ Assert.IsTrue(transport.IsRefCountingEnabled,
+ "Reference counting should be enabled when using client factory constructor");
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public void HandlerFactoryConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled()
+ {
+ // Arrange
+ Func handlerFactory = _ => new HttpClientHandler();
+
+ // Act
+ using var transport = new HttpClientTransport(handlerFactory);
+
+ // Assert - Reference counting should be enabled and updates should work
+ Assert.IsTrue(transport.IsRefCountingEnabled,
+ "Reference counting should be enabled when using handler factory constructor");
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public void DefaultConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled()
+ {
+ // Act
+ using var transport = new HttpClientTransport();
+
+ // Assert - Default constructor should enable reference counting and updates via factory
+ Assert.IsTrue(transport.IsRefCountingEnabled,
+ "Reference counting should be enabled when using default constructor");
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public async Task ProcessAsync_WithRefCountingDisabled_DoesNotCallTryAddRef()
+ {
+ // Arrange
+ var mockHandler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK));
+ using var transport = new HttpClientTransport(mockHandler);
+
+ var request = transport.CreateRequest();
+ request.Uri.Reset(new Uri("https://example.com"));
+ var message = new HttpMessage(request, ResponseClassifier.Shared);
+
+ // Act & Assert - This should not throw even though we're not managing ref counts
+ await ProcessSyncOrAsync(transport, message);
+
+ Assert.AreEqual(200, message.Response.Status);
+ }
+
+ [Test]
+ public async Task ProcessAsync_WithRefCountingEnabled_CallsTryAddRefAndRelease()
+ {
+ // Arrange
+ var mockHandler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK));
+ Func handlerFactory = _ => mockHandler;
+ using var transport = new HttpClientTransport(handlerFactory);
+
+ var request = transport.CreateRequest();
+ request.Uri.Reset(new Uri("https://example.com"));
+ var message = new HttpMessage(request, ResponseClassifier.Shared);
+
+ // Act
+ await ProcessSyncOrAsync(transport, message);
+
+ // Assert - Should succeed with ref counting
+ Assert.AreEqual(200, message.Response.Status);
+ }
+
+ [Test]
+ public async Task ConcurrentRequestsWithUpdate_RefCountingEnabled_SafeDisposal()
+ {
+ // Arrange
+ var requestCount = 0;
+ var responseCount = 0;
+ Func clientFactory = _ =>
+ {
+ var handler = new MockHttpHandler(req =>
+ {
+ Interlocked.Increment(ref requestCount);
+ // Simulate some processing time
+ Thread.Sleep(10);
+ Interlocked.Increment(ref responseCount);
+ return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
+ });
+ return new HttpClient(handler);
+ };
+
+ using var transport = new HttpClientTransport(clientFactory);
+
+ // Act - Start multiple concurrent requests and update transport during processing
+ var tasks = new List();
+
+ // Start several requests
+ for (int i = 0; i < 5; i++)
+ {
+ tasks.Add(Task.Run(async () =>
+ {
+ var request = transport.CreateRequest();
+ request.Uri.Reset(new Uri("https://example.com"));
+ var message = new HttpMessage(request, ResponseClassifier.Shared);
+ await ProcessSyncOrAsync(transport, message);
+ Assert.AreEqual(200, message.Response.Status);
+ }));
+ }
+
+ // Update transport while requests are in flight
+ await Task.Delay(5); // Let some requests start
+ transport.Update(new HttpPipelineTransportOptions());
+
+ // Wait for all requests to complete
+ await Task.WhenAll(tasks);
+
+ // Assert - All requests should complete successfully
+ Assert.AreEqual(5, responseCount, "All 5 requests should have completed");
+ }
+
+ [Test]
+ public void Update_WithRefCountingDisabled_DoesNotThrowForDirectClientConstructor()
+ {
+ // Arrange
+ using var client = new HttpClient();
+ using var transport = new HttpClientTransport(client);
+
+ // Act & Assert - Update should not throw for direct client, but should be a no-op
+ Assert.IsFalse(transport.IsRefCountingEnabled,
+ "Reference counting should be disabled for direct client constructor");
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public void Update_WithRefCountingEnabled_SucceedsWithFactory()
+ {
+ // Arrange
+ Func clientFactory = _ => new HttpClient();
+ using var transport = new HttpClientTransport(clientFactory);
+
+ // Act & Assert - Reference counting should be enabled and updates should succeed
+ Assert.IsTrue(transport.IsRefCountingEnabled,
+ "Reference counting should be enabled when using client factory");
+ Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions()));
+ }
+
+ [Test]
+ public async Task DisposedTransportWithRefCounting_ThrowsObjectDisposedException()
+ {
+ // Arrange
+ Func clientFactory = _ => new HttpClient();
+ var transport = new HttpClientTransport(clientFactory);
+
+ var request = transport.CreateRequest();
+ request.Uri.Reset(new Uri("https://example.com"));
+ var message = new HttpMessage(request, ResponseClassifier.Shared);
+
+ // Act - Dispose the transport
+ transport.Dispose();
+
+ // Assert - Subsequent requests should throw
+ var ex = await AssertThrowsAsync(
+ async () => await ProcessSyncOrAsync(transport, message));
+ Assert.AreEqual(nameof(HttpClientTransport), ex.ObjectName);
+ }
+
+ [Test]
+ public async Task MultipleUpdatesWithConcurrentRequests_HandlesSafeDisposal()
+ {
+ // Arrange
+ var updateCount = 0;
+ Func clientFactory = _ =>
+ {
+ Interlocked.Increment(ref updateCount);
+ var handler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK));
+ return new HttpClient(handler);
+ };
+
+ using var transport = new HttpClientTransport(clientFactory);
+
+ // Act - Multiple updates and concurrent requests
+ var tasks = new List();
+
+ // Start continuous requests
+ for (int i = 0; i < 10; i++)
+ {
+ tasks.Add(Task.Run(async () =>
+ {
+ for (int j = 0; j < 5; j++)
+ {
+ var request = transport.CreateRequest();
+ request.Uri.Reset(new Uri("https://example.com"));
+ var message = new HttpMessage(request, ResponseClassifier.Shared);
+ await ProcessSyncOrAsync(transport, message);
+ Assert.AreEqual(200, message.Response.Status);
+ await Task.Delay(1); // Small delay between requests
+ }
+ }));
+ }
+
+ // Perform updates while requests are running
+ for (int i = 0; i < 3; i++)
+ {
+ await Task.Delay(5);
+ transport.Update(new HttpPipelineTransportOptions());
+ }
+
+ // Wait for all requests to complete
+ await Task.WhenAll(tasks);
+
+ // Assert - Should have created multiple clients due to updates
+ Assert.GreaterOrEqual(updateCount, 4, "Should have created multiple clients due to updates");
+ }
+
+ #region Helper Methods
+
+ private async Task ProcessSyncOrAsync(HttpClientTransport transport, HttpMessage message)
+ {
+ if (IsAsync)
+ {
+ await transport.ProcessAsync(message);
+ }
+ else
+ {
+ transport.Process(message);
+ }
+ }
+
+ private static async Task AssertThrowsAsync(Func action) where T : Exception
+ {
+ try
+ {
+ await action();
+ Assert.Fail($"Expected exception of type {typeof(T).Name} but none was thrown");
+ return null; // Never reached
+ }
+ catch (T ex)
+ {
+ return ex;
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Expected exception of type {typeof(T).Name} but got {ex.GetType().Name}: {ex.Message}");
+ return null; // Never reached
+ }
+ }
+
+ #endregion
+
+ #region Mock Classes
+
+ private class MockDisposableHttpClient : HttpClient
+ {
+ private readonly Action _onDispose;
+
+ public MockDisposableHttpClient(Action onDispose) : base(new HttpClientHandler())
+ {
+ _onDispose = onDispose;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _onDispose?.Invoke();
+ }
+ base.Dispose(disposing);
+ }
+ }
+
+ private class MockHttpHandler : HttpMessageHandler
+ {
+ private readonly Func _responseFactory;
+
+ public MockHttpHandler(Func responseFactory)
+ {
+ _responseFactory = responseFactory;
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(_responseFactory(request));
+ }
+
+#if NET5_0_OR_GREATER
+ protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _responseFactory(request);
+ }
+#endif
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs
index 4c297eefd861..319c0acea610 100644
--- a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs
+++ b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Pipeline;
@@ -334,6 +335,28 @@ public async Task RequestContextDefault_IsErrorIsSet(int code, bool isError)
Assert.AreEqual(isError, response.IsError);
}
+ [Test]
+ public void TransportIsUpdatedWhenPolicyFiresTransportUpdatedEvent()
+ {
+ var policy = new TransportUpdatingPolicy();
+ var options = new HttpPipelineTransportOptions();
+ var certificate = GetTestCertificate();
+ options.ClientCertificates.Add(certificate);
+
+ var mockTransport = new MockTransport(
+ new MockResponse(1));
+
+ var _ = new HttpPipeline(mockTransport, [
+ policy
+ ], responseClassifier: new CustomResponseClassifier());
+
+ Assert.That(mockTransport.TransportUpdates, Is.Empty);
+
+ policy.UpdateTransport(options);
+
+ Assert.AreEqual(certificate, options.ClientCertificates[0]);
+ }
+
#region Helpers
public class AddHeaderPolicy : HttpPipelineSynchronousPolicy
{
@@ -377,6 +400,44 @@ public override bool IsErrorResponse(HttpMessage message)
// How classifiers will be generated in DPG.
private static ResponseClassifier _responseClassifier200204304;
private static ResponseClassifier ResponseClassifier200204304 => _responseClassifier200204304 ??= new StatusCodeClassifier(stackalloc ushort[] { 200, 204, 304 });
+
+ public class TransportUpdatingPolicy : HttpPipelinePolicy, ISupportsTransportUpdate
+ {
+ public event Action TransportOptionsChanged;
+
+ public void UpdateTransport(HttpPipelineTransportOptions options)
+ {
+ TransportOptionsChanged?.Invoke(options);
+ }
+
+ public TransportUpdatingPolicy()
+ {
+ }
+
+ public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
+ }
+
+ public override void Process(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ ProcessNext(message, pipeline);
+ }
+ }
+
+ private X509Certificate2 GetTestCertificate()
+ {
+ byte[] cer = Convert.FromBase64String(Pfx);
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cer, null);
+#else
+ return new X509Certificate2(cer);
+#endif
+ }
+
+ private const string Pfx = @"
+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";
#endregion
}
diff --git a/sdk/core/Azure.Core/tests/PipelineTestBase.cs b/sdk/core/Azure.Core/tests/PipelineTestBase.cs
index d75ad0518e97..9c3312c4c32a 100644
--- a/sdk/core/Azure.Core/tests/PipelineTestBase.cs
+++ b/sdk/core/Azure.Core/tests/PipelineTestBase.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Pipeline;
@@ -63,6 +65,17 @@ protected async Task ExecuteRequest(HttpMessage message, HttpPipeline
return message.Response;
}
+ protected X509Certificate2 GetCertificate(string pfx)
+ {
+ byte[] cer = Convert.FromBase64String(pfx);
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cer, null);
+#else
+ return new X509Certificate2(cer);
+#endif
+ }
+
protected const string Pfx = @"
MIIQ5gIBAzCCEKIGCSqGSIb3DQEHAaCCEJMEghCPMIIQizCCCowGCSqGSIb3DQEHAaCCCn0Eggp5
MIIKdTCCCnEGCyqGSIb3DQEMCgECoIIJfjCCCXowHAYKKoZIhvcNAQwBAzAOBAg6i6mCUbwdbAIC
@@ -141,5 +154,8 @@ protected async Task ExecuteRequest(HttpMessage message, HttpPipeline
SFOZJoMiZA9U17D0bxXn3LUImJn9MhBs5m3G9/A7C/M6y0PAdsnzHro/LpC17zbnJ1zOMDswHzAH
BgUrDgMCGgQUwVWF1Axq6WtJTF05+H4d6s1JX64EFBqoVUD5ZqKahaRAXLZ6WX9DqGmhAgIH0AAA
AAAAAAAA";
+
+ protected const string Pfx2 = @"
+MIIQzwIBAzCCEIUGCSqGSIb3DQEHAaCCEHYEghByMIIQbjCCBloGCSqGSIb3DQEHBqCCBkswggZHAgEAMIIGQAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBC7yCMtx6oDATpCaQVf5XC5AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQa2eU6J3q3Uso5/ADdc5jpICCBdDtAYxjkhyCrCI7iKVUDTsu9qY/ZBkAZtndloT18lYUjOiV/eP5Mg6x+bxzWaN6z32ZZ/GIlqihemVuhwSBsp28tA4dkeK7md2xbCrr1WaAdX0VgUG0/3CpYmI8023o37rD3mq8I2vpB8svnkrFeu1vbDyGbTNmdym3UUIvyawLAXxzEjTKjRlYKyD+TwEzqWqYS1xEFDtG9g7OVdsbc7nqJiH84I2JqpYHwGaWKrli0R0YIOhbiMXo8MNPLP5DjH0JzJ0OkL059qQSYACWLrfNggSu7VMo/AXIorx3gWSfeUolBW1HMU4UwoOHy4VuNBv9yF108plAYAlrfYbeY+ZuBP5IWjvQh2svms7M7Eutol+O4moibN1HeKVPTob3qHX+GMW8rhr55Ii6t2JzQbR447ZCw+5FfLLN9QjWGbVuz2e1Kq/zgMvKCnMjjNJuJPWgTRTo9h02JTt/af2YwB/005p4O4q3Se3HfB1hTQv2dBWvAnUbaqMt0aplQjklWgbczhZMgj5X/t93+/jACbZSEhVgnK8iaTHGndSJnIwVSAG6eMvF+wdFNZjGghaglzfP+72PBXHndsSWYdGwzQZtFlW3b9eA9UxmMbV7VipXTC2V8/jmeAt5dY8X/mG8wZFsTaxAs2Z5cTJFP3KvoSAYegXUOHLWzA5CDug9mARzkLACFLsOo4STINsUI1BDItBJ3dbDuZ/5++zIwcr4ediG2GKeyFaw1gAOgWXi5/Qc3p9MHCB5EGTPaXPjq6qMZJJAyF2K7zlOxRjYTXLdWeK3fe6N6EvQfORGtDlEmrd0gLgOoAMfoZFNtHgpwAe7IChx6Cz+DUzIzi7dZT0OOhXK2eB92FZ4EkJjwH/Njv+AZwypqC1txrdRzI+gEnIeGEkh3OAfWu/WUMeNLUrJk87tIkGgHqPqp6/86/nZe362O8DmbX4BTiYUz2nDiIxfq95uvzLKz20cj3kp6nCUq04utvpYATN+ZogIQjHmxaZqKp7MvCx3L91id+P7gxDZxd22kEjpJnKjgDoZU6eO8MvE/Wb5cr7fGurRRhpzmZ23/JGJ2caMEaLSyV0yxiEB5jS+ikvMAvupcUHnirDGA+76GV6GuL2/2fS/ottGqtp3O2jL69onCGx3UMA5jjIRPVZseCSmERdjrSnetJTGc61VTw39xbvSewsOuYJlL+ruddL8UJzRmu5/NcIw9xq/4waX6g2QJvnfO9xZHJdAat5XG4ie5ePmYNtok+xONV8ZhWKAv3UmuLmnzcHuqN0mxBniiLsFJBSfay4/UbPhL+vXeE4Al/gyk70I07w2fIVhxG7h5MTo+juWv8OqmbnQ1p96Aq6QY5DcV9Tg9FW2coivzkX7aA5nHUGGt/GowgEDePdJjBgaXOq1XXDZpgJotYWyFqKYHvI+hvFaHrqSIlbyariG+LTInjhmJa1/WcJGoEB1ngFhX9up0tqBQdxaocxB/2jprNww9ioqbIwE9vTEBfM/WFfprHjx+beVEKJW3KCWDkyQMItD7V/ps8KvrAhm7cR9yOXfl3KOya1qCPo77TkFR3WrWFXtufCzYMW1ewj2MxNxMmkb8+/TA+FxW/NkOmlPFekfzsegjTyT4m2DJo0t3MrSJLsEsL4MOtO+8dRM7rP8HFMZmkpiiy1IYZ69n1jKl6De1Y6R5h1vhdGtMpgmwF9GGpFCeX7ErbF8duH8oiHwobtvv3GL0XBN73sFpYIaDFvsGmXGh/UZ1LKCznenteWg5UN+hgQ5jW8f/hKd92E2kyl30jXwOnJLbryN/BfP8db3+Opz55A17zxPMMKYKBHiSGp9ncbFJ/zg65bNiUXuJam/P5/6VjEii+gLqnzMLY4D94y/kEVvU/hoT1X7+tNYF42xxPcDOQs4ew5pe4caGpo9oW2yLdsXUhWWNBrxg+13/w6hXC2924YoeyeHCA6BEQSmlD3uLC7vBmHuea3eTCOwrP6MrVOm3HIwggoMBgkqhkiG9w0BBwGgggn9BIIJ+TCCCfUwggnxBgsqhkiG9w0BDAoBAqCCCbkwggm1MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBC7i0BgvYExXtovt+QPCERsAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ0lWlUzm/ZafkFQD8G75nGQSCCVCfn5F3SRe4xqHuY93dD4dzbmRUpToVa6MC6jbJwP4jcDEth1BCZnIcdDxNOrwlXBeoFudsDOjfa2mVaSi96hxJTxLCqCIauXXKyg2JPYiHQaMvT3BGDpFOe6b9MgN0nVwi8LnvN3GWTCR1bYDZnitIxkmwIfSayPSYuG0qdn9lVRrEA8qevMyPZvxxU6OZadhFTurd9+GwaqF29t9k90m15Sj0ZgR+Ox956k9xdaJ/WakbF/5YE806qfibU7mn1+5EGY2rTp8tEvE30eaUrM1sIg2L6pOx9oh4Rdm2tTN/DkamBaCPWLhPOJ8LiIRtWAOTvJ3slG+A12Xnot698R0BtixkQCE6UiCsDMU6uTb7wAUn5m/VPYDQo2y+sftOiBSve9s+1Sui+M72HXJMFVXXZAivUGapUOr19sNS06IP44OzeHNwXQL2cUfFqU0JKK6sqEFlTdQYkwu7Fr7s5XxiGIykj7C6Z592POL4hj4gFQC4Tn5vatYWaRr6oNmTnP8a1AFl6LhQBCgbiiYw4fczsG4sqi3Z+6B9024L9kwIUeQCnXyZRb8AFukTgR1o9brBR0mcpANb2w0mESysTti7cwvC+PCrfY6SyBsJLtFX5SkQLK9SBhMczlvwhfVOSsUiN4K+BEAZjCnjoXuV5xjoUHF5//q8fQkLxQsAIvwgVydkekhXoEiGJ7BQ8NbW5R86ibvgXRW5V+cAEsQZcMXMGEBvWAVGHAOCu5zpDkTFSyED9SjmWbMNW9PNrJZFFTLI4HNtwwdhOaABEvcYlP9vhZj79rfbdPu+cDoDzo+KtzpSzq10uNupUo6ODojmuq3onCiB2bFl/e29MhD9+8E0yx+Tkd4RaJSXnQqDEhpJiuBT12lAWKAjyDIGs0Cr9uD5AQR4vpIJf1/MROIr2vmSS5b5kLkkA8T9L0VJQ9FaL2ZTG+D+e0JnhO7U8JnngRktZsu3XQkbIwYP0dsRpSOcuvmAO92mmPtBQgrrPAKoMhvwl+CX1EBTNybQ5BCXF0dgO/1htyg7TtdnokBx+gCexsMASs4dcOVCUtNK9D2s8OvZGIAGvGB4mQc61gp5qpzGwsPGaImvqhdT0dMJB4V1XIxwM4wED9kfRW01Unhk8XAktL9wwM+pRHCy7+RnFVNH8+b6/6Q+ryYm0wyLmjL3nEPTggkrOHdh+NkNfgQRj9l9PWNo+BJ5uRpPNmE5z0IQkmhJVcLCdgsmlSZZmmBA3VnVUim03pImV6t978plH/HfH58pK762TN09tdb1fCUCePgg8HFqR9zWtPHrmHhax5hEFYxlYt4RBUyxcDYXTYUnbOBb6hPrsmJcQs+IMP2vhjsKdZz267srqegB5GVdBkSyn35NE2w7iDzuyFvb8UsYjqBg9WCVy7pmLJ5mv/3SerrumRda0ILiL2hwrQIrKgYEZJY8giiCvWjru2NJW3q5PbIwyM27DTCkQXPp6NN09FBXwLhi+qXeNMmzQuREE9QnTQcTGbIBfiRc7Avn0y4jz4etLqTTqJLPLX9lU0JwN1LJ5grjNdNWFNHc3NpxfOEesmIkyPkmVkSgYL4ldEicDM7ur05Fr8kH0Lzpnj0ZmeRdX4Ggrx4OKuyKJ5EWTVQSrroJqIrLxWvzq70ANI6uO3nk8hdTEkL6F9pX79mjyJuSj5cNRGtgLvliZTm4wSk/qNBEUySj1fhtWHcLb8ggurnVcmBCvM8IZCVVjW+hi20mPt3jp+Es7pyKq6DI5h/gn9gIXZAN1RWVtEakK6w4p1DqRFM4twc4VY77hUKRb03P0XPsewON1tCGIwScZ2+fo/1f5kvhlXf2Mw03O5kAIzCd11QOnLs/3NOWoV7xEQP8+pp8HkwAiiy2KUS1JDVH2Wo1pfw9wUDkdu5Si+7XzALy6aIFlHiYGs5gEWZZiSVRbetSeCeD2PJlRuaosfozxJa2eNgdERC9eOoAEKbn0iwFGcNswMsaZoSLBvHMByxoYYF++uSRJIR60hOAPxT4Qi4UVU/wkDztEM1tLve0ByIGLQAkyv7xfHzPuRhWT0MZCsLMoJW9iCZwa2DSB9Jkkotf1jHO84UCt0OBDLYMNht6+FIHw/BEmRTn22kdKpskrD+VCdrxYL5UNEt5d57vz3M++hcFE4dNEYzv0TfYl7GNRRvw6qLz3j1JIgCXoxhQ8a/DxHe91hJk3NcM8inQ1v6a0f+BxL/xFx4wNvdoDZsp6u++MRsDj+dwG51x/ysFwtDIKuhOzG0chJcLWxmqglfjpL6A6OyBu0o9O/ZmdUgfEBMkxCHuVR7Xk2ASmMoBgAVHKsd2UuPhVIX6T4ONLfhFICSTTnHLiGkeoO4BHdaGRd09caNQEc0GBIUfwEP7L7hifqZOTAK9gmJZcfCzu2qiqY7kOferQpw76GwfVW/kOV0FmXD4IQR6myXt/AkqsJgim6dswWI66iKQ2ec30ApVv855D+0i40RfsUGSbGvcwxxdG78Tc4hafBwA3F9fKYOrrDGRCMrnktGG1caJRpSWmNEv4+q15ceSLF05NEO9bqtc52g1t5mtdlDDrMu91kmvCs4X9nLao3y+klwZwIK4ttQRNWD8bWN7EUns/iM8P7E3IrXZgBIAdxCalOD3PnU65He8CeMCh/ZnmPIz5eBwRFRKj5N2S4U8XCBzme3oG5jBH74r5hCsOOZt93ulejV8hJosLW5Rq9fHZfPi6IvcKSQbHg+muNOlSRb9aSihDSy9FitPoOdX4mLXmPKC0OlcXOMVcG6RvoEtOs6yKnD+uPdOmnZkmEWnxQ/EmgDreRIieFgLfNkO52aIaWgg2nTU530O9uTWBSk3QSCKF4n/NWCWmjZiWg5O3CmUcxoKI4eRmZ58oYvAl+wdI4k+89rV1jUmmts8O3eftAHWeUiXWils2MdzWbFk0iwdsutavh8prEbGZfAM6nnRhR0L4ppN5dvSRWtjI5eyxfuODnGUskGcSAXD0zdqfJTMqg1ey/qF51vYDKwyk9uq8jSESdgA9Rc1oCBMqRfFgDHmPnlfdmIMO2bp8VrZKWNTPBTzCIf00jUlUpNNtwnMb0dpZwk3rPHfgIdMLQ8SNxkts+VqTTEmIIb2dmC1353s5AMNydrtyNAXYHSSp9hxwP8oESZdQRlUjivFusGPpIS/spcEt6JBu7aiMmSfKTElMCMGCSqGSIb3DQEJFTEWBBQLglDeakGMXukKkw1Ak5ac4o4cLzBBMDEwDQYJYIZIAWUDBAIBBQAEIJUPNvPN11c4V/Bq+3kIUB9hj37CScdpGtoQMf3htjMqBAhX75eHkXy8wgICCAA=";
}
}
diff --git a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs
index 1864fd1c3729..154b93bb7a5a 100644
--- a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs
+++ b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Security;
using System.Runtime.ConstrainedExecution;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
@@ -32,6 +33,8 @@ public TransportFunctionalTests(bool isAsync) : base(isAsync)
{
}
+ internal static RemoteCertificateValidationCallback certCallback = (_, _, _, _) => true;
+
protected abstract HttpPipelineTransport GetTransport(bool https = false, HttpPipelineTransportOptions options = null);
public static object[] ContentWithLength =>
@@ -227,9 +230,9 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod
Assert.AreEqual(expectedMethod, httpMethod);
}
- public static object[] HeadersWithValues =>
- new object[]
- {
+ public static object[] HeadersWithValues =>
+ new object[]
+ {
// Name, value, is content, supports multiple
new object[] { "Allow", "adcde", true, true },
new object[] { "Accept", "adcde", true, true },
@@ -256,50 +259,50 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod
new object[] { "Range", "bytes=0-100", false, false },
new object[] { "Content-Length", "16", true, false },
new object[] { "Date", "Tue, 12 Nov 2019 08:00:00 GMT", false, false },
- };
+ };
- [TestCaseSource(nameof(HeadersWithValues))]
- public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
- {
+ [TestCaseSource(nameof(HeadersWithValues))]
+ public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
+ {
StringValues httpHeaderValues = default;
- using TestServer testServer = new TestServer(
- context =>
- {
- Assert.True(context.Request.Headers.TryGetValue(headerName, out httpHeaderValues));
- });
+ using TestServer testServer = new TestServer(
+ context =>
+ {
+ Assert.True(context.Request.Headers.TryGetValue(headerName, out httpHeaderValues));
+ });
- var transport = GetTransport();
- Request request = CreateRequest(transport, testServer);
+ var transport = GetTransport();
+ Request request = CreateRequest(transport, testServer);
- request.Headers.Add(headerName, headerValue);
+ request.Headers.Add(headerName, headerValue);
- if (contentHeader)
- {
- request.Content = RequestContent.Create(new byte[16]);
- }
+ if (contentHeader)
+ {
+ request.Content = RequestContent.Create(new byte[16]);
+ }
- Assert.True(request.Headers.TryGetValue(headerName, out var value));
- Assert.AreEqual(headerValue, value);
+ Assert.True(request.Headers.TryGetValue(headerName, out var value));
+ Assert.AreEqual(headerValue, value);
- Assert.True(request.Headers.TryGetValue(headerName.ToUpper(), out value));
- Assert.AreEqual(headerValue, value);
+ Assert.True(request.Headers.TryGetValue(headerName.ToUpper(), out value));
+ Assert.AreEqual(headerValue, value);
- CollectionAssert.AreEqual(new[]
- {
+ CollectionAssert.AreEqual(new[]
+ {
new HttpHeader(headerName, headerValue),
}, request.Headers);
- await ExecuteRequest(request, transport);
+ await ExecuteRequest(request, transport);
- Assert.AreEqual(headerValue, string.Join(",", (string[])httpHeaderValues));
- }
+ Assert.AreEqual(headerValue, string.Join(",", (string[])httpHeaderValues));
+ }
- [TestCaseSource(nameof(HeadersWithValues))]
- public async Task CanGetAndAddRequestHeadersUppercase(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
- {
- await CanGetAndAddRequestHeaders(headerName.ToUpperInvariant(), headerValue, contentHeader, supportsMultiple);
- }
+ [TestCaseSource(nameof(HeadersWithValues))]
+ public async Task CanGetAndAddRequestHeadersUppercase(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
+ {
+ await CanGetAndAddRequestHeaders(headerName.ToUpperInvariant(), headerValue, contentHeader, supportsMultiple);
+ }
[TestCaseSource(nameof(HeadersWithValues))]
public void TryGetReturnsCorrectValuesWhenNotFound(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
@@ -317,7 +320,8 @@ public void TryGetReturnsCorrectValuesWhenNotFound(string headerName, string hea
[TestCaseSource(nameof(HeadersWithValues))]
public async Task CanAddMultipleValuesToRequestHeader(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
{
- if (!supportsMultiple) return;
+ if (!supportsMultiple)
+ return;
var anotherHeaderValue = headerValue + "1";
var joinedHeaderValues = headerValue + "," + anotherHeaderValue;
@@ -384,7 +388,8 @@ public async Task CanGetAndSetResponseHeaders(string headerName, string headerVa
[TestCaseSource(nameof(HeadersWithValues))]
public async Task CanGetAndSetMultiValueResponseHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple)
{
- if (!supportsMultiple) return;
+ if (!supportsMultiple)
+ return;
var anotherHeaderValue = headerValue + "1";
var joinedHeaderValues = headerValue + "," + anotherHeaderValue;
@@ -523,7 +528,7 @@ public async Task CanGetAndSetContentStream()
});
var stream = new MemoryStream();
- stream.SetLength(10*1024);
+ stream.SetLength(10 * 1024);
var content = RequestContent.Create(stream);
var transport = GetTransport();
@@ -537,7 +542,7 @@ public async Task CanGetAndSetContentStream()
await ExecuteRequest(request, transport);
CollectionAssert.AreEqual(stream.ToArray(), requestBytes);
- Assert.AreEqual(10*1024, requestBytes.Length);
+ Assert.AreEqual(10 * 1024, requestBytes.Length);
}
public static object[][] RequestMethods => new[]
@@ -689,13 +694,13 @@ public async Task StreamRequestContentCanBeSentMultipleTimes()
await ExecuteRequest(request, transport);
await ExecuteRequest(request, transport);
- Assert.AreEqual(new long [] {3, 3}, requests);
+ Assert.AreEqual(new long[] { 3, 3 }, requests);
}
[Test]
public async Task RequestContentIsNotDisposedOnSend()
{
- using TestServer testServer = new TestServer(context =>{});
+ using TestServer testServer = new TestServer(context => { });
DisposeTrackingContent disposeTrackingContent = new DisposeTrackingContent();
var transport = GetTransport();
@@ -1089,14 +1094,7 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien
// This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset.
ServicePointManager.ServerCertificateValidationCallback = null;
#endif
- byte[] cer = Convert.FromBase64String(Pfx);
- X509Certificate2 clientCert;
-
-#if NET9_0_OR_GREATER
- clientCert = X509CertificateLoader.LoadPkcs12(cer, null);
-#else
- clientCert = new X509Certificate2(cer);
-#endif
+ X509Certificate2 clientCert = GetCertificate(Pfx);
using (TestServer testServer = new TestServer(
async context =>
@@ -1105,6 +1103,7 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien
if (setClientCertificate)
{
Assert.NotNull(cert);
+ Assert.AreEqual(clientCert, cert);
}
else
{
@@ -1130,6 +1129,130 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien
}
}
+ [Test]
+ public async Task ClientCertificateCanBeRotatedFromEmpty()
+ {
+#if NETFRAMEWORK // ServicePointManager is obsolete and doesn't affect HttpClient
+ // This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset.
+ ServicePointManager.ServerCertificateValidationCallback = null;
+#endif
+ X509Certificate2 clientCert = GetCertificate(Pfx);
+ bool setClientCertificate = false;
+ bool validatedCert = false;
+
+ using (TestServer testServer = new TestServer(
+ async context =>
+ {
+ var cert = context.Connection.ClientCertificate;
+ if (setClientCertificate)
+ {
+ Assert.NotNull(cert);
+ Assert.AreEqual(clientCert, cert);
+ validatedCert = true;
+ }
+ else
+ {
+ Assert.Null(cert);
+ }
+ byte[] buffer = Encoding.UTF8.GetBytes("Hello");
+ await context.Response.Body.WriteAsync(buffer, 0, buffer.Length);
+ },
+ true))
+ {
+ var options = new HttpPipelineTransportOptions();
+ options.ServerCertificateCustomValidationCallback = args => true;
+
+ var transport = GetTransport(true, options);
+
+ // Initially no client certificate
+ Request request = transport.CreateRequest();
+ request.Uri.Reset(testServer.Address);
+
+ await ExecuteRequest(request, transport);
+
+ // Now set the client certificate and update the transport
+ options.ClientCertificates.Add(clientCert);
+
+ transport.Update(options);
+ setClientCertificate = true;
+
+ request = transport.CreateRequest();
+ request.Uri.Reset(testServer.Address);
+
+ await ExecuteRequest(request, transport);
+ Assert.IsTrue(validatedCert, "Client certificate was not validated after transport update.");
+ }
+ }
+
+ [Test]
+ public async Task ClientCertificateCanBeRotatedFromExistingCert()
+ {
+ try
+ {
+#if NETFRAMEWORK // ServicePointManager is obsolete and doesn't affect HttpClient
+ // This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset.
+ ServicePointManager.ServerCertificateValidationCallback -= certCallback;
+#endif
+ X509Certificate2 clientCert = GetCertificate(Pfx);
+ X509Certificate2 anotherCert = GetCertificate(Pfx2);
+ bool setClientCertificate = false;
+ bool validatedCert = false;
+
+ using (TestServer testServer = new TestServer(
+ async context =>
+ {
+ var cert = context.Connection.ClientCertificate;
+ if (setClientCertificate)
+ {
+ Assert.NotNull(cert);
+ Assert.AreEqual(anotherCert, cert);
+ validatedCert = true;
+ }
+ else
+ {
+ Assert.NotNull(cert);
+ Assert.AreEqual(clientCert, cert);
+ validatedCert = true;
+ }
+ byte[] buffer = Encoding.UTF8.GetBytes("Hello");
+ await context.Response.Body.WriteAsync(buffer, 0, buffer.Length);
+ },
+ true))
+ {
+ var options = new HttpPipelineTransportOptions();
+ options.ServerCertificateCustomValidationCallback = args => true;
+ options.ClientCertificates.Add(clientCert);
+
+ var transport = GetTransport(true, options);
+
+ // Initially the first client certificate
+ Request request = transport.CreateRequest();
+ request.Uri.Reset(testServer.Address);
+
+ await ExecuteRequest(request, transport);
+
+ // Now set the client certificate and update the transport
+ options.ClientCertificates.Clear();
+ options.ClientCertificates.Add(anotherCert);
+
+ transport.Update(options);
+ setClientCertificate = true;
+
+ request = transport.CreateRequest();
+ request.Uri.Reset(testServer.Address);
+
+ await ExecuteRequest(request, transport);
+ Assert.IsTrue(validatedCert, "Client certificate was not validated after transport update.");
+ }
+ }
+ finally
+ {
+#if NETFRAMEWORK
+ ServicePointManager.ServerCertificateValidationCallback += certCallback;
+#endif
+ }
+ }
+
[Test]
public async Task No100ContinueSentByDefault()
{