AutodiscoverSiteScope and Client-only sites

The Exchange Autodiscover service is very smart when it hands out URLs for web services to clients.  It will always hand out URLs that are in the same Active Directory site as the user's mailbox server.  Unfortunately, when it comes to Outlook locating Autodiscover originally, the client just isn't that smart.  The way a domain-joined client can find a local Autodiscover service is by utilizing the AutodiscoverSiteScope information that is part of the Service-Connection-Point (SCP) object in Active Directory.

This AutodiscoverSiteScope attribute must be populated manually using the Set-ClientAccessServer cmdlet.
Set-ClientAccessServer -identity <name of CAS> -AutodiscoverSiteScope Site1,Site2,Site3

At install, this attribute is populated with the AD Site of it's corresponding CAS.  That way any clients that are in the same AD Site as the CAS will be able to use the local server to make Autodiscover Requests. 

The problem occurs when you have AD Sites that only have Outlook clients (and not Exchange Servers).  By default, they will just use a Random CAS for Autodiscover (or the first one comes back from AD in the LDAP query).  To select a site for these clients to use, you can add the "Client site" to the AutodiscoverSiteScope Attribute on each CAS in the closest AD site.  That way you conserve bandwidth and improve performance by keeping Autodiscover traffic relatively local.

For more information on configuring the AutodiscoverSiteScope attribute and it's uses, see: How to Configure the Autodiscover Service to Use Site Affinity.

I was recently presented with the situation of a customer who had over 2000 AD sites with Outlook clients but a relatively small number of sites with Exchange servers.  They were faced with the potential of having to add thousands of AD Sites to the AutodiscoverSiteScope by hand.  This got me to thinking that this could be automated with a small amount of effort. 

Below is a sample of automating that process.  This PowerShell script makes use of a command-line tool (CalcSiteCost.exe) that uses the NTDS API's DsQuerySitesByCost to calculate the cost between each client site and each Exchange Site.

The syntax used for CalcSiteCost.exe is: CalcSiteCost.exe fromSite toSite1 [toSite2 toSite3 ...] .  The results are returned as simple integers (one per line) that are printed to the console.  The PowerShell script parses the console output and casts each one to an integer and adds it to an int[].

Both the sample script and the sample source and binary for CalcSiteCost are available as an attachment to the post.

# Check for verbose param
if($Args -contains "-verbose" -or $Args -contains "-vb")
 $VerbosePreference = "Continue";

# load required .NET Assemblies
$temp = [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices")

# Get current forest info
$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest();

# Build list of sites that have Exchange Servers
[string[]]$exchangeSites = @();

Get-ExchangeServer | Where {$_.serverrole -contains "ClientAccess"} | foreach { $exchangeSites += $_.Site.Name; }

# or you can manually set the Exchange sites if you prefer
# $exchangeSites = "Charlotte","Redmond"

# Loop through each Exchange Site
foreach($clientsite in $forest.sites)

 $leastCostExchangeName = "";
 $leastCostExchangeCost = 99999;
 [int[]] $costArray = @();
 .\CalcSiteCost.exe $clientsite.Name $exchangeSites | foreach { $costArray += [Int32]::Parse($_); }

 # find the lowest cost and note the site 
 for($i=0; $i -lt $costArray.Length; $i++)
  if($costArray[$i] -lt $leastCostExchangeCost)
   $leastCostExchangeCost = $costArray[$i];
   $leastCostExchangeName = $exchangeSites[$i];
 write-host ("Least cost Exchange Site for client site " + $clientsite.Name + " is " + $leastCostExchangeName + " Cost: " +$leastCostExchangeCost);
 # Get the list of
 $closestCASList = Get-ExchangeServer | Where {$_.serverrole -contains "ClientAccess"} | where {$ -eq $leastCostExchangeName};
 foreach($closeCas in $closestCASList)
  # Get existing attribute
  $clientsiteScopeList = (Get-ClientAccessServer -identity $closeCas.Name).AutoDiscoverSiteScope; 

  # big special case for $null
  if($clientsiteScopeList -eq $null)
   Set-ClientAccessServer -identity $closeCas.Name -AutoDiscoverSiteScope "";
   $clientsiteScopeList = (Get-ClientAccessServer -identity $closeCas.Name).AutoDiscoverSiteScope;
  # If It's not already in the list, add it.
  if($clientsiteScopeList -notcontains $clientsite.Name) { $clientsiteScopeList.Add($clientsite.Name); }
  # Set the updated list back to using Set-ClientAccessServer
  Set-ClientAccessServer -identity $closeCas.Name -AutodiscoverSiteScope $clientsiteScopeList