diff --git a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs index 7b2ab4d65b..33490963ea 100644 --- a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.Cosmos using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; + using System.Net.Http; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; @@ -423,6 +424,15 @@ public PortReuseMode? PortReuseMode set; } + /// + /// Gets or sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication. + /// + public Func HttpClientFactory + { + get; + set; + } + /// /// (Direct/TCP) This is an advanced setting that controls the number of TCP connections that will be opened eagerly to each Cosmos DB back-end. /// diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index d5d4ec4a95..614c9d91fe 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos using System.Data.Common; using System.Linq; using System.Net; + using System.Net.Http; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; @@ -66,6 +67,7 @@ public class CosmosClientOptions private int? maxTcpConnectionsPerEndpoint; private PortReuseMode? portReuseMode; private IWebProxy webProxy; + private Func httpClientFactory; /// /// Creates a new CosmosClientOptions @@ -321,6 +323,11 @@ public IWebProxy WebProxy { throw new ArgumentException($"{nameof(this.WebProxy)} requires {nameof(this.ConnectionMode)} to be set to {nameof(ConnectionMode.Gateway)}"); } + + if (this.HttpClientFactory != null) + { + throw new ArgumentException($"{nameof(this.WebProxy)} cannot be set along {nameof(this.HttpClientFactory)}"); + } } } @@ -426,6 +433,32 @@ public CosmosSerializer Serializer /// public bool EnableTcpConnectionEndpointRediscovery { get; set; } = false; + /// + /// Gets or sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication. + /// + /// + /// + /// HTTPS communication is used when is set to for all operations and when is (default) for metadata operations. + /// + /// + /// Useful in scenarios where the application is using a pool of HttpClient instances to be shared, like ASP.NET Core applications with IHttpClientFactory or Blazor WebAssembly applications. + /// + /// + [JsonIgnore] + public Func HttpClientFactory + { + get => this.httpClientFactory; + set + { + if (this.WebProxy != null) + { + throw new ArgumentException($"{nameof(this.HttpClientFactory)} cannot be set along {nameof(this.WebProxy)}"); + } + + this.httpClientFactory = value; + } + } + /// /// Gets or sets the connection protocol when connecting to the Azure Cosmos service. /// @@ -556,7 +589,8 @@ internal ConnectionPolicy GetConnectionPolicy() MaxTcpConnectionsPerEndpoint = this.MaxTcpConnectionsPerEndpoint, EnableEndpointDiscovery = !this.LimitToEndpoint, PortReuseMode = this.portReuseMode, - EnableTcpConnectionEndpointRediscovery = this.EnableTcpConnectionEndpointRediscovery + EnableTcpConnectionEndpointRediscovery = this.EnableTcpConnectionEndpointRediscovery, + HttpClientFactory = this.httpClientFactory }; if (this.ApplicationRegion != null) @@ -737,6 +771,11 @@ private string GetUserAgentFeatures() features |= CosmosClientOptionsFeatures.AllowBulkExecution; } + if (this.HttpClientFactory != null) + { + features |= CosmosClientOptionsFeatures.HttpClientFactory; + } + if (features == CosmosClientOptionsFeatures.NoFeatures) { return null; diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptionsFeatures.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptionsFeatures.cs index 02eef19178..b81b80b252 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptionsFeatures.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptionsFeatures.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos internal enum CosmosClientOptionsFeatures { NoFeatures = 0, - AllowBulkExecution = 1 + AllowBulkExecution = 1, + HttpClientFactory = 2 } } diff --git a/Microsoft.Azure.Cosmos/src/DocumentClient.cs b/Microsoft.Azure.Cosmos/src/DocumentClient.cs index 4c830876e7..1f86e7cc78 100644 --- a/Microsoft.Azure.Cosmos/src/DocumentClient.cs +++ b/Microsoft.Azure.Cosmos/src/DocumentClient.cs @@ -1117,7 +1117,23 @@ private async Task GetInitializationTaskAsync(IStoreClientFactory storeClientFac this.EnsureValidOverwrite(this.desiredConsistencyLevel.Value); } - GatewayStoreModel gatewayStoreModel = new GatewayStoreModel( + GatewayStoreModel gatewayStoreModel; + if (this.ConnectionPolicy.HttpClientFactory != null) + { + gatewayStoreModel = new GatewayStoreModel( + this.GlobalEndpointManager, + this.sessionContainer, + this.ConnectionPolicy.RequestTimeout, + (Cosmos.ConsistencyLevel)this.accountServiceConfiguration.DefaultConsistencyLevel, + this.eventSource, + this.serializerSettings, + this.ConnectionPolicy.UserAgentContainer, + this.ApiType, + this.ConnectionPolicy.HttpClientFactory); + } + else + { + gatewayStoreModel = new GatewayStoreModel( this.GlobalEndpointManager, this.sessionContainer, this.ConnectionPolicy.RequestTimeout, @@ -1127,6 +1143,7 @@ private async Task GetInitializationTaskAsync(IStoreClientFactory storeClientFac this.ConnectionPolicy.UserAgentContainer, this.ApiType, this.httpMessageHandler); + } this.GatewayStoreModel = gatewayStoreModel; diff --git a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs index 44a18f4ee7..b8f0e4bf77 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/CosmosClientBuilder.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Fluent { using System; using System.Net; + using System.Net.Http; using Microsoft.Azure.Cosmos.Core.Trace; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; @@ -391,6 +392,26 @@ public CosmosClientBuilder WithBulkExecution(bool enabled) return this; } + /// + /// Sets a delegate to use to obtain an HttpClient instance to be used for HTTPS communication. + /// + /// A delegate function to generate instances of HttpClient. + /// + /// + /// HTTPS communication is used when is set to for all operations and when is (default) for metadata operations. + /// + /// + /// Useful in scenarios where the application is using a pool of HttpClient instances to be shared, like ASP.NET Core applications with IHttpClientFactory or Blazor WebAssembly applications. + /// + /// + /// The object + /// + public CosmosClientBuilder WithHttpClientFactory(Func httpClientFactory) + { + this.clientOptions.HttpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + return this; + } + /// /// Provider that allows encrypting and decrypting data. /// See https://aka.ms/CosmosClientEncryption for more information on client-side encryption support in Azure Cosmos DB. diff --git a/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs b/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs index fea5a68ef2..1888d5ccd7 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs @@ -31,24 +31,80 @@ internal class GatewayStoreModel : IStoreModel, IDisposable private GatewayStoreClient gatewayStoreClient; private CookieContainer cookieJar; - public GatewayStoreModel( + private GatewayStoreModel( GlobalEndpointManager endpointManager, ISessionContainer sessionContainer, - TimeSpan requestTimeout, ConsistencyLevel defaultConsistencyLevel, - DocumentClientEventSource eventSource, - JsonSerializerSettings serializerSettings, - UserAgentContainer userAgent, - ApiType apiType = ApiType.None, - HttpMessageHandler messageHandler = null) + DocumentClientEventSource eventSource) { // CookieContainer is not really required, but is helpful in debugging. this.cookieJar = new CookieContainer(); this.endpointManager = endpointManager; - HttpClient httpClient = new HttpClient(messageHandler ?? new HttpClientHandler { CookieContainer = this.cookieJar }); this.sessionContainer = sessionContainer; this.defaultConsistencyLevel = defaultConsistencyLevel; + this.eventSource = eventSource; + } + + public GatewayStoreModel( + GlobalEndpointManager endpointManager, + ISessionContainer sessionContainer, + TimeSpan requestTimeout, + ConsistencyLevel defaultConsistencyLevel, + DocumentClientEventSource eventSource, + JsonSerializerSettings serializerSettings, + UserAgentContainer userAgent, + ApiType apiType, + HttpMessageHandler messageHandler) + : this(endpointManager, + sessionContainer, + defaultConsistencyLevel, + eventSource) + { + this.InitializeGatewayStoreClient( + requestTimeout, + serializerSettings, + userAgent, + apiType, + new HttpClient(messageHandler ?? new HttpClientHandler { CookieContainer = this.cookieJar })); + } + public GatewayStoreModel( + GlobalEndpointManager endpointManager, + ISessionContainer sessionContainer, + TimeSpan requestTimeout, + ConsistencyLevel defaultConsistencyLevel, + DocumentClientEventSource eventSource, + JsonSerializerSettings serializerSettings, + UserAgentContainer userAgent, + ApiType apiType, + Func httpClientFactory) + : this(endpointManager, + sessionContainer, + defaultConsistencyLevel, + eventSource) + { + HttpClient httpClient = httpClientFactory(); + if (httpClient == null) + { + throw new InvalidOperationException("HttpClientFactory did not produce an HttpClient"); + } + + this.InitializeGatewayStoreClient( + requestTimeout, + serializerSettings, + userAgent, + apiType, + httpClient); + + } + + private void InitializeGatewayStoreClient( + TimeSpan requestTimeout, + JsonSerializerSettings serializerSettings, + UserAgentContainer userAgent, + ApiType apiType, + HttpClient httpClient) + { // Use max of client specified and our own request timeout value when sending // requests to gateway. Otherwise, we will have gateway's transient // error hiding retries are of no use. @@ -65,12 +121,10 @@ public GatewayStoreModel( httpClient.DefaultRequestHeaders.Add(HttpConstants.HttpHeaders.Accept, RuntimeConstants.MediaTypes.Json); - this.eventSource = eventSource; this.gatewayStoreClient = new GatewayStoreClient( httpClient, this.eventSource, serializerSettings); - } public virtual async Task ProcessMessageAsync(DocumentServiceRequest request, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs index cb56e26825..ea61806e3c 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ClientTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -301,6 +302,45 @@ await Assert.ThrowsExceptionAsync(async () => { DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseAsync(Guid.NewGuid().ToString()); }); } + + [TestMethod] + public async Task HttpClientFactorySmokeTest() + { + HttpClient client = new HttpClient(); + Mock> factory = new Mock>(); + factory.Setup(f => f()).Returns(client); + CosmosClient cosmosClient = new CosmosClient( + ConfigurationManager.AppSettings["GatewayEndpoint"], + ConfigurationManager.AppSettings["MasterKey"], + new CosmosClientOptions + { + ApplicationName = "test", + ConnectionMode = ConnectionMode.Gateway, + ConnectionProtocol = Protocol.Https, + HttpClientFactory = factory.Object + } + ); + + string someId = Guid.NewGuid().ToString(); + Cosmos.Database database = null; + try + { + database = await cosmosClient.CreateDatabaseAsync(someId); + Cosmos.Container container = await database.CreateContainerAsync(Guid.NewGuid().ToString(), "/id"); + await container.CreateItemAsync(new { id = someId }); + await container.ReadItemAsync(someId, new Cosmos.PartitionKey(someId)); + await container.DeleteItemAsync(someId, new Cosmos.PartitionKey(someId)); + await container.DeleteContainerAsync(); + Mock.Get(factory.Object).Verify(f => f(), Times.Once); + } + finally + { + if (database!= null) + { + await database.DeleteAsync(); + } + } + } } internal static class StringHelper diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/UserAgentTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/UserAgentTests.cs index 8d728b43ba..0733d887a3 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/UserAgentTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/UserAgentTests.cs @@ -110,10 +110,13 @@ public async Task VerifyUserAgentWithFeatures(bool useMacOs) this.SetEnvironmentInformation(useMacOs); const string suffix = " UserApplicationName/1.0"; + CosmosClientOptionsFeatures featuresFlags = CosmosClientOptionsFeatures.NoFeatures; + featuresFlags |= CosmosClientOptionsFeatures.AllowBulkExecution; + featuresFlags |= CosmosClientOptionsFeatures.HttpClientFactory; - string features = Convert.ToString((int)CosmosClientOptionsFeatures.AllowBulkExecution, 2).PadLeft(8, '0'); + string features = Convert.ToString((int)featuresFlags, 2).PadLeft(8, '0'); - using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(true))) + using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(true).WithHttpClientFactory(() => new HttpClient()))) { Cosmos.UserAgentContainer userAgentContainer = client.ClientOptions.GetConnectionPolicy().UserAgentContainer; @@ -130,7 +133,7 @@ public async Task VerifyUserAgentWithFeatures(bool useMacOs) await db.DeleteAsync(); } - using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix).WithBulkExecution(false))) + using (CosmosClient client = TestCommon.CreateCosmosClient(builder => builder.WithApplicationName(suffix))) { Cosmos.UserAgentContainer userAgentContainer = client.ClientOptions.GetConnectionPolicy().UserAgentContainer; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs index 606eae3742..c71d2f0af5 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -21,6 +21,7 @@ public class CosmosClientOptionsUnitTests { public const string AccountEndpoint = "https://localhost:8081/"; public const string ConnectionString = "AccountEndpoint=https://localtestcosmos.documents.azure.com:443/;AccountKey=425Mcv8CXQqzRNCgFNjIhT424GK99CKJvASowTnq15Vt8LeahXTcN5wt3342vQ==;"; + public Func HttpClientFactoryDelegate = () => new HttpClient(); [TestMethod] public void VerifyCosmosConfigurationPropertiesGetUpdated() @@ -74,6 +75,7 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() Assert.IsNull(clientOptions.WebProxy); Assert.IsFalse(clientOptions.LimitToEndpoint); Assert.IsFalse(clientOptions.EnableTcpConnectionEndpointRediscovery); + Assert.IsNull(clientOptions.HttpClientFactory); //Verify GetConnectionPolicy returns the correct values for default ConnectionPolicy policy = clientOptions.GetConnectionPolicy(); @@ -87,6 +89,7 @@ public void VerifyCosmosConfigurationPropertiesGetUpdated() Assert.IsNull(policy.MaxTcpConnectionsPerEndpoint); Assert.IsTrue(policy.EnableEndpointDiscovery); Assert.IsFalse(policy.EnableTcpConnectionEndpointRediscovery); + Assert.IsNull(policy.HttpClientFactory); cosmosClientBuilder.WithApplicationRegion(region) .WithConnectionModeGateway(maxConnections, webProxy) @@ -353,6 +356,41 @@ public void VerifyApplicationRegionSettingsWithPreferredRegions() cosmosClientOptions.GetConnectionPolicy(); } + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void VerifyWebProxyHttpClientFactorySet() + { + CosmosClientOptions cosmosClientOptions = new CosmosClientOptions(); + cosmosClientOptions.WebProxy = Mock.Of(); + cosmosClientOptions.HttpClientFactory = () => new HttpClient(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void VerifyHttpClientFactoryWebProxySet() + { + CosmosClientOptions cosmosClientOptions = new CosmosClientOptions(); + cosmosClientOptions.HttpClientFactory = () => new HttpClient(); + cosmosClientOptions.WebProxy = Mock.Of(); + } + + [TestMethod] + public void HttpClientFactoryBuildsConnectionPolicy() + { + string endpoint = AccountEndpoint; + string key = Guid.NewGuid().ToString(); + CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( + accountEndpoint: endpoint, + authKeyOrResourceToken: key) + .WithHttpClientFactory(this.HttpClientFactoryDelegate); + CosmosClient cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); + CosmosClientOptions clientOptions = cosmosClient.ClientOptions; + + Assert.AreEqual(clientOptions.HttpClientFactory, this.HttpClientFactoryDelegate); + ConnectionPolicy policy = clientOptions.GetConnectionPolicy(); + Assert.AreEqual(policy.HttpClientFactory, this.HttpClientFactoryDelegate); + } + [TestMethod] public void WithLimitToEndpointAffectsEndpointDiscovery() { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json index 27589cc42c..a5b1ffb912 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json @@ -1482,6 +1482,18 @@ ], "MethodInfo": "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.RequestHandler] get_CustomHandlers()" }, + "System.Func`1[System.Net.Http.HttpClient] get_HttpClientFactory()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Func`1[System.Net.Http.HttpClient] get_HttpClientFactory()" + }, + "System.Func`1[System.Net.Http.HttpClient] HttpClientFactory[Newtonsoft.Json.JsonIgnoreAttribute()]": { + "Type": "Property", + "Attributes": [ + "JsonIgnoreAttribute" + ], + "MethodInfo": null + }, "System.Net.IWebProxy get_WebProxy()": { "Type": "Method", "Attributes": [], @@ -1673,6 +1685,11 @@ "Attributes": [], "MethodInfo": "Void set_GatewayModeMaxConnectionLimit(Int32)" }, + "Void set_HttpClientFactory(System.Func`1[System.Net.Http.HttpClient])": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Void set_HttpClientFactory(System.Func`1[System.Net.Http.HttpClient])" + }, "Void set_IdleTcpConnectionTimeout(System.Nullable`1[System.TimeSpan])": { "Type": "Method", "Attributes": [], @@ -2640,6 +2657,11 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithCustomSerializer(Microsoft.Azure.Cosmos.CosmosSerializer)" }, + "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithHttpClientFactory(System.Func`1[System.Net.Http.HttpClient])": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithHttpClientFactory(System.Func`1[System.Net.Http.HttpClient])" + }, "Microsoft.Azure.Cosmos.Fluent.CosmosClientBuilder WithLimitToEndpoint(Boolean)": { "Type": "Method", "Attributes": [], diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs index 26541aed46..387ae365d1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs @@ -270,6 +270,62 @@ private async Task GatewayStoreModel_Exception_UpdateSessionTokenOnKnownExceptio } } + [TestMethod] + public void GatewayStoreModel_HttpClientFactory() + { + HttpClient staticHttpClient = new HttpClient(); + + Mock> mockFactory = new Mock>(); + mockFactory.Setup(f => f()).Returns(staticHttpClient); + + Mock mockDocumentClient = new Mock(); + mockDocumentClient.Setup(client => client.ServiceEndpoint).Returns(new Uri("https://foo")); + + GlobalEndpointManager endpointManager = new GlobalEndpointManager(mockDocumentClient.Object, new ConnectionPolicy()); + SessionContainer sessionContainer = new SessionContainer(string.Empty); + DocumentClientEventSource eventSource = DocumentClientEventSource.Instance; + GatewayStoreModel storeModel = new GatewayStoreModel( + endpointManager, + sessionContainer, + TimeSpan.FromSeconds(5), + ConsistencyLevel.Eventual, + eventSource, + null, + new UserAgentContainer(), + ApiType.None, + mockFactory.Object); + + Mock.Get(mockFactory.Object) + .Verify(f => f(), Times.Once); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void GatewayStoreModel_HttpClientFactory_IfNull() + { + HttpClient staticHttpClient = null; + + Mock> mockFactory = new Mock>(); + mockFactory.Setup(f => f()).Returns(staticHttpClient); + + Mock mockDocumentClient = new Mock(); + mockDocumentClient.Setup(client => client.ServiceEndpoint).Returns(new Uri("https://foo")); + + GlobalEndpointManager endpointManager = new GlobalEndpointManager(mockDocumentClient.Object, new ConnectionPolicy()); + SessionContainer sessionContainer = new SessionContainer(string.Empty); + DocumentClientEventSource eventSource = DocumentClientEventSource.Instance; + GatewayStoreModel storeModel = new GatewayStoreModel( + endpointManager, + sessionContainer, + TimeSpan.FromSeconds(5), + ConsistencyLevel.Eventual, + eventSource, + null, + new UserAgentContainer(), + ApiType.None, + mockFactory.Object); + } + [TestMethod] // Verify that for 429 exceptions, session token is not updated public async Task GatewayStoreModel_Exception_NotUpdateSessionTokenOnKnownExceptions()