From e859ae71b77c6fc2646722ba58d062e13f16bc04 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 6 Sep 2023 10:17:48 -0700 Subject: [PATCH] Cosmos: Add a way to configure Application Preferred Regions Fixes #29424 --- .../CosmosDbContextOptionsBuilder.cs | 11 ++++++++ .../Internal/CosmosDbOptionExtension.cs | 26 +++++++++++++++++++ .../Internal/CosmosSingletonOptions.cs | 11 ++++++++ .../Internal/ICosmosSingletonOptions.cs | 8 ++++++ .../Internal/SingletonCosmosClientWrapper.cs | 5 ++++ .../CosmosDbContextOptionsExtensionsTests.cs | 2 ++ 6 files changed, 63 insertions(+) diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index b396ce16b2f..e26e454ba6d 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -65,6 +65,17 @@ public virtual CosmosDbContextOptionsBuilder ExecutionStrategy( public virtual CosmosDbContextOptionsBuilder Region(string region) => WithOption(e => e.WithRegion(Check.NotNull(region, nameof(region)))); + /// + /// Configures the context to use the provided preferred regions for geo-replicated database accounts. + /// + /// + /// See Using DbContextOptions, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// A list of Azure Cosmos DB region names. + public virtual CosmosDbContextOptionsBuilder PreferredRegions(IReadOnlyList regions) + => WithOption(e => e.WithPreferredRegions(Check.NotNull(regions, nameof(regions)))); + /// /// Limits the operations to the provided endpoint. /// diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs index 33c4c7588f0..9a7792b57e5 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs @@ -23,6 +23,7 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension private string? _connectionString; private string? _databaseName; private string? _region; + private IReadOnlyList? _preferredRegions; private ConnectionMode? _connectionMode; private bool? _limitToEndpoint; private Func? _executionStrategyFactory; @@ -61,6 +62,7 @@ protected CosmosOptionsExtension(CosmosOptionsExtension copyFrom) _databaseName = copyFrom._databaseName; _connectionString = copyFrom._connectionString; _region = copyFrom._region; + _preferredRegions = copyFrom._preferredRegions; _connectionMode = copyFrom._connectionMode; _limitToEndpoint = copyFrom._limitToEndpoint; _executionStrategyFactory = copyFrom._executionStrategyFactory; @@ -247,6 +249,30 @@ public virtual CosmosOptionsExtension WithRegion(string? region) return clone; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? PreferredRegions + => _preferredRegions; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual CosmosOptionsExtension WithPreferredRegions(IReadOnlyList? regions) + { + var clone = Clone(); + + clone._preferredRegions = regions; + + return clone; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs index 22f657166c6..4426c1ecc9c 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Net; using Azure.Core; @@ -54,6 +55,14 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions /// public virtual string? Region { get; private set; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? PreferredRegions { get; private set; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -158,6 +167,7 @@ public virtual void Initialize(IDbContextOptions options) TokenCredential = cosmosOptions.TokenCredential; ConnectionString = cosmosOptions.ConnectionString; Region = cosmosOptions.Region; + PreferredRegions = cosmosOptions.PreferredRegions; LimitToEndpoint = cosmosOptions.LimitToEndpoint; EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite; ConnectionMode = cosmosOptions.ConnectionMode; @@ -188,6 +198,7 @@ public virtual void Validate(IDbContextOptions options) || TokenCredential != cosmosOptions.TokenCredential || ConnectionString != cosmosOptions.ConnectionString || Region != cosmosOptions.Region + || !StructuralComparisons.StructuralEqualityComparer.Equals(PreferredRegions, cosmosOptions.PreferredRegions) || LimitToEndpoint != cosmosOptions.LimitToEndpoint || ConnectionMode != cosmosOptions.ConnectionMode || WebProxy != cosmosOptions.WebProxy diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs index b38e4f40993..a26b79a82b3 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs @@ -60,6 +60,14 @@ public interface ICosmosSingletonOptions : ISingletonOptions /// string? Region { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IReadOnlyList? PreferredRegions { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs index bc4f620c922..4e99f2e0b00 100644 --- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs @@ -41,6 +41,11 @@ public SingletonCosmosClientWrapper(ICosmosSingletonOptions options) configuration.ApplicationRegion = options.Region; } + if (options.PreferredRegions != null) + { + configuration.ApplicationPreferredRegions = options.PreferredRegions; + } + if (options.LimitToEndpoint != null) { configuration.LimitToEndpoint = options.LimitToEndpoint.Value; diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index 963951c2409..708056c3270 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -71,6 +71,8 @@ public void Can_create_options_with_valid_values() Test(o => o.Region(Regions.EastAsia), o => Assert.Equal(Regions.EastAsia, o.Region)); // The region will be validated by the Cosmos SDK, because the region list is not constant Test(o => o.Region("FakeRegion"), o => Assert.Equal("FakeRegion", o.Region)); + Test(o => o.PreferredRegions(new[] { Regions.AustraliaCentral, Regions.EastAsia }), + o => Assert.Equal(new[] { Regions.AustraliaCentral, Regions.EastAsia }, o.PreferredRegions)); Test(o => o.ConnectionMode(ConnectionMode.Direct), o => Assert.Equal(ConnectionMode.Direct, o.ConnectionMode)); Test(o => o.GatewayModeMaxConnectionLimit(3), o => Assert.Equal(3, o.GatewayModeMaxConnectionLimit)); Test(o => o.MaxRequestsPerTcpConnection(3), o => Assert.Equal(3, o.MaxRequestsPerTcpConnection));