I ran in to an issue today trying to assign some scopes to one of the standard display groups (Search Dropdown) on a newly created site collection, and ran in to this little problem:

$group=$scopes.GetDisplayGroup($siteUrl,"Search Dropdown")
Error Calling GetDisplayGroup with 2 arguments, Specified cast is not valid

A quick Google yielded some examples but no solutions other than manually adding the groups, or visiting /_layouts/viewscope.aspx – which inexplicably fixed the problem.

As others had discovered, inspecting all display groups (or using GetDisplayGroupsForSite) confirmed no display groups present for that site, yet when viewing /_layouts/viewscopes.aspx the groups are present (and subsequently also available via code). So, is viewscopes.aspx actually creating the scopes if they don't exist?

It sure does! Kind of. Starting from InitializeGridView in the code behind for viewscopes.aspx, here are the relevant calls:

// Microsoft.Office.Server.Search.Internal.UI.ViewScopesPage.InitializeGridView(bool) (called from OnPreRender)

// Microsoft.Office.Server.Search.Administration.ScopesUtilities.EnsureConsumer(ScopesManager)
scopesManager.Consumers.EnsureRegistered(asConsumerName); // asConsumerName=SPSite.Id.ToString("D")

(Lots of boring calls to register the consumer, until we get to…)

// Microsoft.Office.Server.Search.Administration.Scopes.OnConsumerAdded(Consumer)
Scope scope = this.AllScopes [1]; // magic!
Uri url = consumer.Url;
ScopeDisplayGroup scopeDisplayGroup = this.AllDisplayGroups.UnprotectedCreate("Search Dropdown", "(description)", url, true, true);
if (scope != null)
  scopeDisplayGroup.UnprotectedAdd (scope);
  scopeDisplayGroup.UnprotectedSetDefault (scope);

(Note: calls are abbreviated a bit just for clarity, they pull in resource strings and do all sorts of other magic).

Great, loading viewscopes.aspx creates the 'Search Dropdown', 'Advanced Search' and 'Site Directories' display groups against the site collection (eventually). So all I have to do is call an appropriate method somewhere along the line and have the scopes created for me. Right? Not a chance. All methods are private or within internal classes, and the only entry points are from ASPX page code behinds. Yuck!

But all is not lost. Thanks to this page, I can actually invoke the private methods, and get these scopes created:

# Pull in our references
Add-PSSnapIn Microsoft.SharePoint.Powershell -ErrorAction:SilentlyContinue
[reflection.assembly]::LoadWithPartialName("Microsoft.Office.Server") | Out-Null

# Get our site, search context and scopes
$site = Get-SPSite $siteUrl
$siteId = $site.Id.ToString("D")
$ctx = [Microsoft.Office.Server.Search.Administration.SearchContext]::GetContext($site)
$scopes = New-Object Microsoft.Office.Server.Search.Administration.Scopes($ctx)

# Here's the cool stuff. We're really just calling $scopes.Consumers.EnsureRegistered($siteId)
$BindingFlags = [Reflection.BindingFlags] "NonPublic,Instance"

# $consumers = $scopes.Consumers
$consumers = $scopes.GetType().GetMember("Consumers",$BindingFlags)[0].GetValue($scopes, $null)
# $consumers.EnsureRegistered(…)
$consumer = $consumers.GetType().GetMethod("EnsureRegistered",$BindingFlags).Invoke($consumers, $siteId)

It works beautifully! Now, you may be thinking this is overkill. Why not just do one of the following:

  1. Use powershell hit /_layouts/viewscopes.aspx (which will handle all of this nonsense for us)
  2. Create the scopes yourself, instead of sorcery invoking private methods Here's why:

Hit viewscopes.aspx via PowerShell

Yes, this is a viable solution (and arguably a less hacky one). However, if this is a new site collection, there will probably be significant warm up time waiting for the w3wp processes and what not to fire up, so you may have to try hitting the URL a few times. It also annoys me that I have to make a dumb web request to get something done (and by "dumb" I mean blindly hitting a URL, not caring about input and disregarding the output). I'm developing against this platform, I should be able to do this via code!

Create the scopes myself via PowerShell

Seems fair. However the code in OnConsumerAdded unfortunately isn't expecting something else to be creating its scopes, and doesn't cater for it. When I said, OnConsumerAdded adds the scopes, it does just that. It doesn't check to see if they exist and create otherwise, it just attempts to create them, and an exception is thrown if they're already there. Which means the first time someone visits viewscopes.aspx, they're going to get an ugly error message (and potentially, not have the site collection registered as a consumer, and I don't even know what that means! :-) ). The assumption seems to be that no-one is going to try to programmatically use display groups until a human as actually accessed that page. No, that would never happen! /s

Furthermore, these scopes are SharePoint specific display groups. You shouldn't have to be dealing with that yourself. That's like having to manually create your Pages library because the publishing feature will only do it for you if it's a Tuesday. And you're wearing a pink shirt.

So for me, sneakily calling Scopes.Consumers.EnureRegistered() is the best solution. It won't break viewscopes.aspx and you're not replicating the work SharePoint already does (as much) to create display groups.

The last thing which bugged me was this: why is GetDisplayGroup throwing an illegal cast exception? Some people suggested permissions (not being able to access SPSite.Url), but I think if that were the case, you'd have much bigger problems anyway, you're trying to manipulate search display groups after all!

It's actually nothing to do with this method, or what you're passing to it. GetDisplayGroup makes a call to GetDisplayGroupIDFromName(string siteId, string name), which in turn calls stored proc dbo.proc_MSS_GetScopeDisplayGroupIDFromName to return the ID of the group name, like so:

result = (int)sqlCommand.Parameters ["@DisplayGroupID"].Value;

However, if the group doesn't exist (for instance, no one's hit viewscopes.aspx yet), the stored proc returns System.DBNull. And gosh darn it, trying to cast System.DBNull.Value to an int isn't taken too kindly 'round these parts. And that's where the invalid cast exception comes from.

So I guess the take away message here is this: don't use GetDisplayGroup unless you're damn certain the display group exists. If you're unsure, it might be better to use GetDisplayGroupForSite:

$groups = $scopes.GetDisplayGroupForSite($site.Url) 
[email protected]($groups | ?{ $_.Name -eq "Search Dropdown" })[0]
if ( -not $group ) {
  // create it