@@ -42,6 +42,7 @@ public class TunnelManagementClient : ITunnelManagementClient
4242 private const string EventsApiSubPath = "/events" ;
4343 private const string ClustersApiPath = "/clusters" ;
4444 private const string ClustersV1ApiPath = ApiV1Path + "/clusters" ;
45+ private const string RecommendationsSubPath = "/recommendations" ;
4546 private const string TunnelAuthenticationScheme = "Tunnel" ;
4647 private const string RequestIdHeaderName = "VsSaaS-Request-Id" ;
4748 private const string CheckAvailableSubPath = ":checkNameAvailability" ;
@@ -1084,6 +1085,29 @@ public async Task<Tunnel> CreateTunnelAsync(
10841085 {
10851086 Requires . NotNull ( tunnel , nameof ( tunnel ) ) ;
10861087 options ??= new TunnelRequestOptions ( ) ;
1088+
1089+ // If the caller didn't specify a cluster, auto-select one via the
1090+ // recommendations API. Failures fall back to global routing.
1091+ if ( string . IsNullOrEmpty ( tunnel . ClusterId ) )
1092+ {
1093+ try
1094+ {
1095+ var recommendations = await GetClusterRecommendationsAsync (
1096+ preferredClusterId : null ,
1097+ requiredGeo : options . RequiredGeo ,
1098+ cancellation ) ;
1099+ if ( ! string . IsNullOrEmpty ( recommendations ? . RecommendedClusterId ) )
1100+ {
1101+ tunnel . ClusterId = recommendations ! . RecommendedClusterId ;
1102+ }
1103+ }
1104+ catch ( Exception ) when ( ! cancellation . IsCancellationRequested )
1105+ {
1106+ // Fall through to global (Traffic Manager) routing if the
1107+ // recommendations request fails for any reason.
1108+ }
1109+ }
1110+
10871111 options . AdditionalHeaders ??= new List < KeyValuePair < string , string > > ( ) ;
10881112 options . AdditionalHeaders = options . AdditionalHeaders . Append (
10891113 new KeyValuePair < string , string > ( "If-None-Match" , "*" ) ) ;
@@ -1595,6 +1619,46 @@ public async Task<ClusterDetails[]> ListClustersAsync(CancellationToken cancella
15951619 return clusterDetails ! ;
15961620 }
15971621
1622+ /// <inheritdoc/>
1623+ public async Task < ClusterRecommendationResponse > GetClusterRecommendationsAsync (
1624+ string ? preferredClusterId = null ,
1625+ string ? requiredGeo = null ,
1626+ CancellationToken cancellation = default )
1627+ {
1628+ var baseAddress = this . httpClient . BaseAddress ! ;
1629+ var builder = new UriBuilder ( baseAddress ) ;
1630+ builder . Path = ClustersPath + RecommendationsSubPath ;
1631+
1632+ var queryParts = new List < string > ( ) ;
1633+ var apiQuery = GetApiQuery ( ) ;
1634+ if ( ! string . IsNullOrEmpty ( apiQuery ) )
1635+ {
1636+ queryParts . Add ( apiQuery ! ) ;
1637+ }
1638+
1639+ if ( ! string . IsNullOrEmpty ( preferredClusterId ) )
1640+ {
1641+ queryParts . Add (
1642+ $ "preferredClusterId={ Uri . EscapeDataString ( preferredClusterId ! ) } ") ;
1643+ }
1644+
1645+ if ( ! string . IsNullOrEmpty ( requiredGeo ) )
1646+ {
1647+ queryParts . Add ( $ "requiredGeo={ Uri . EscapeDataString ( requiredGeo ! ) } ") ;
1648+ }
1649+
1650+ builder . Query = string . Join ( "&" , queryParts ) ;
1651+
1652+ var response = await SendRequestAsync < object , ClusterRecommendationResponse > (
1653+ HttpMethod . Get ,
1654+ builder . Uri ,
1655+ options : null ,
1656+ authHeader : null ,
1657+ body : null ,
1658+ cancellation ) ;
1659+ return response ! ;
1660+ }
1661+
15981662 /// <inheritdoc/>
15991663 public async Task < bool > CheckNameAvailabilityAsync (
16001664 string name ,
0 commit comments