Friday 19 August 2011

SP 2010 user profile service app - ILM Database could not be created

This week I've been battling with the following error during a SharePoint 2010 user profile service app setup: ILM Database could not be created.

I had created some powershell scripts to set up the user profile service applications automatically on multiple farms. These scripts worked like a charm on most farms, except one.
The service application itself was correctly created, but when the script tried to start the User Profile Sync Service it failed. The service itself briefly went to a Started state, but reverted back to its Stopped state after a few minutes.

A glance in the event logs revealed a number of warnings, that were logged at the time the service started:

Warning 1
ILM SQL Configuration file could not be read: 
Error Parcing the SQL File: C:\Program Files\Microsoft Office Servers\14.0\Sql\StoredProcedures.sql  

Exception: Conversion of one or more characters from XML to target collation impossible

Warning 2
ILM Database could not be created: Error sent to Windows Event Log running : 
-- Proc Name:	GetObjectDisplayNames
-- Purpose:		to get display names for objects
-- Input:		@ObjectIDs, which we suppose to take in an array of Guids, in the format of:
--				(?B553ECA-F359-11DB-AABA-D0C055D89593? ... , �A5B106DC-F359-11DB-9C44-1EC155D89593?
-- Exception:
--				If failed to find certain object id, SQL error occurs
CREATE PROCEDURE [dbo].[GetObjectDisplayNames]
@ObjectIDs     NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Sql	NVARCHAR(MAX)
SET @Sql =
N' SELECT ValueString FROM ObjectValues ' +
N' WHERE AttributeName = N''DisplayName'' ' +
N' AND ObjectID IN ' + @ObjectIDs
EXEC sp_executesql @Sql
END
: Conversion of one or more characters from XML to target collation impossible

These warnings repeated themselves a few times and then it said: Sync service stopped succesfully. Just brilliant, ain't it?
As I couldn't find any clues online, I presumed I was the first to get this kind of exotic crap.
So in the next days I tried to fix it ... to no avail however. I tried:
  • rerunning the script (who knows, maybe it was a one-time glitch)
  • installing the user profile service app manually
  • reinstalling sharepoint completely
  • installing SP1
  • formatting and reinstalling all servers in that $*@! farm

But finally I found a workaround. It involves tampering with some out-of-the-box installation files, so it will void your warranty/support from Microsoft. Proceed with caution! ;)

Solution
I noticed there was a strange token in the message displayed in the second warning in the event-log.

Could it be that easy? Was this token the source of all that "Conversion of one or more characters from XML to target collation impossible" crap?
I was pretty desperate at that time, so I was willing to give it a shot. Here's what I did:

  1. Open an explorer window and go to C:\Program Files\Microsoft Office Servers\14.0\Sql
  2. Locate the file StoredProcedures.sql, this contains the stored proc that was mentioned in the error.
  3. Take a backup of this file as we'll have to modify it. Always take a backup if you're going to tinker with ootb SharePoint files ;)
  4. Open the file in notepad and look for the entry CREATE PROCEDURE [dbo].[GetObjectDisplayNames]
  5. If you scroll up a bit, you should notice a line mentioning "... , ’A5B106DC-F359-11DB-9C44-1EC155D89593?"
  6. Now replace that sign in front of the A5B106DC-F359-11DB-9C44-1EC155D89593 guid with a ? char.
  7. Save the file again

After that I reran the script and a miracle happened (*insert praise for whatever deity you believe in*): everything worked like a charm. The sync service started properly after a few minutes and the user profile service app synced properly.

I'm still not sure why this error only occurred on one farm, while all the others installed without any hassle. All I know is that this fixed it for me. Feel free to leave your ideas on why it works in the comments :)


So there you have it, another dirty fix for a SharePoint 2010 issue. Maybe I should rename this blog to Dirty solutions for exotic sharepoint issues ... :/

Note
Either way, I've also opened a case with Microsoft Support themselves, so hopefully they'll fix this in a future update or provide a better solution. Once I get more info from them, I'll let you know.

Tuesday 16 August 2011

VMWare on a machine hosting the Hyper-V role

A quick trick if you want to run VMWare workstation on a machine that's running Hyper-V.
Normally you can't run VMWare when the Hyper-V role is enabled on a machine, as the hypervisor will claim some resources upon startup ... which are also needed by VMWare to run its virtual machines.

But I found the following trick to work like a charm:
  1. Open a command prompt and type the following:
    bcdedit /copy {default} /d "Windows Without Hypervisor"
    The above command should say: The entry was successfully copied to {guid}.
  2. Copy that guid to the clipboard (including the curly braces!).
  3. Finally, execute the following command in the command prompt:
    bcdedit /set {guid} hypervisorlaunchtype off
    Replace {guid} with the GUID that you copied in step 2.

This will create a new entry in your boot menu, called "Windows Without Hypervisor".
  • If you want to run VMWare, just boot your machine using that entry. You won't be able to host HyperV machines, but VMWare will run just fine.
  • If you want to use HyperV, boot your machine using the default boot entry. This will allow you to run Hyper-V machines, but no VMWare.

Yes, the workaround doesn't fix the fact that VMWare vm's can't coexist with HyperV machines ... but for now that's just fine for me. At least now I can run both vm's on my laptop :)

This workaround was also mentioned here, but the original blogpost is no longer available so I just reposted it here. (All credits go to the original poster of course)

Wednesday 20 July 2011

Update - Creating SharePoint 2010 user profile exclusion filters using Powershell

This post is actually a follow-up on my previous post on user profile exclusion filters.

A quick recap of that post:
  • I needed to script the creation of a user profile service application, along with some profile exclusion filters.
  • Unfortunately I couldn't find any existing reference on script the creation of user profile exclusion filters (only guides on how to do this in the central admin pages).
  • Took a peek at the code SharePoint uses to create exclusion filters in central admin, only to see they use internal/hidden classes & methods to create these filters.
  • In the end I couldn't find any public API/powershell cmdlet to create these filters, so ended up writing my own workaround: a small .NET 3.5 project that will invoke the internal/hidden SharePoint 2010 methods using reflection.

Still, for me there was something missing: an easy way to use this workaround in PowerShell. Cause we all know: Powershell rocks! :p

First attempt
Originally I wanted to translate the complete .NET 3.5 workaround into one (or more) ps1 scripts. This would eliminate all need for external assemblies and is just easier to read/fix should something happen.
I managed to translate 99% of the .NET code, which was actually easier than I thought it would be. It was a nice exercise in the usage of reflection and generics within PowerShell 2, so I might make a specific blogpost on this subject later on :)

However, 99% is not 100%. I just couldn't get the final step working: how to assign the FilterSet objects (the actual filters) to the user profile connection.
Internally SharePoint 2010 uses the SetExclusionFilters method - something I had to call using reflection. This worked like a charm from within a .NET project, but I couldn't get PowerShell to cast the objects to the correct internal classes (despite the fact that Get-Member did mention it was casted correctly).
As a result, I kept getting the following error ... no matter what I tried

Exception calling "Invoke" with "2" argument(s): "Parameter count mismatch."
At Functions.ps1:96 char:35
+ $setExclusionFiltersMI.Invoke <<<< ($connection, @($filterSetList)) + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : DotNetMethodException
When I find the time, I'll post the ps1 scripts too ... maybe one of you can solve this mystery.

First Second attempt
Soooo translating it all into ps1 scripts didn't work, but no worries! Time for a workaround .. for a workaround?!
This time I just wrote another .NET project, which defines some custom cmdlets. These cmdlets will then invoke the .NET code that I described in my previous post. On the bright side: the cmdlets can be easily used from within PowerShall and it actually works! :)
(But you're still stuck with a bunch of .NET assemblies that you cannot change on the fly ... can't have it all, I guess.)

Before I start a lengthy description of the cmdlets I developed, I'll post provide a quick overview. Use these links if you can't be a***d / bothered to read manuals :)

The project defines these cmdlets
  • Get-FilterSetCondition
  • Get-FilterSet
  • Set-ExclusionFilter
  • Get-FilterAttributes
  • Get-FilterSetConditionOperators
  • Get-FilterSetOperators

Get-FilterSetCondition
Use this cmdlet to create a new condition for your exclusion filter. These conditions are internally stored as FilterSetCondition objects, hence the naming. You can find an example of this in Example1.ps1 and Example2.ps1
Syntax:
Get-FilterSetCondition -Connection $connection `
                       -ConditionType "user" `
                       -Attribute "userAccountControl" `
                       -Operator "Bit_on" `
                       -Value "2"
Parameters:
  • Connection
    The connection on which you want to create an exclusion filter (Microsoft.Office.Server.UserProfiles.Connection object)
  • ConditionType
    Whether this condition applies to a user-related attribute or a group-related attribute (just like you have separate windows in the UI to specify user and group filters)
  • Attribute
    The attribute on which you want to filter, e.g. company or userAccountControl. Use the Get-FilterAttributes cmdlet to get an overview of all available attributes.
  • Operator
    The operator that you want to use in this condition, e.g. "Bit_on" or "Equals". Use the Get-FilterSetConditionOperators cmdlet to get an overview of all available operators.
  • Value
    The string-value you want to use in this condition.

Get-FilterSet
Use this cmdlet to create a new exclusion filter, based on conditions you've created using the Get-FilterSetCondition cmdlet. Do not mix user-based conditions with group-based conditions though! You can find an example of this in Example1.ps1 and Example2.ps1
Syntax:
Get-FilterSet -FilterType "user" `
              -Operator "or" `
              -Conditions $userConditions
Parameters:
  • FilterType
    Whether this filter contains user-related conditions or a group-related condition
  • Operator
    The operator that you want to use to combine the conditions. Either supply "or" or "and" as value.
  • Conditions
    An array of FilterSetCondition objects (which you created using the Get-FilterSetCondition cmdlet)

Set-ExclusionFilter
Use this cmdlet to assign exclusion filters (created using the Get-FilterSet cmdlet) to an existing user profile connection. You can either assign a user-based exclusion filter, a group-based exclusion filter or both.
You cannot however assign multiple user-based exclusion filters or multiple group-based exclusion filters. You'll have to combine these into a single user-based exclusion filter or a single group-based exclusion filter. An example of this cmdlet can be found in Example1.ps1 and Example2.ps1
Syntax:
Set-ExclusionFilter -Connection $connection `
                    -UserFilter $userFilter `
                    -GroupFilter $groupFilter
Parameters:
  • Connection
    A reference to a user profile connection (Microsoft.Office.Server.UserProfiles.Connection object)
  • UserFilter
    The user-related exclusion filter you want to set on the connection. Don't supply this parameter if you don't want to set a user-related exclusion filter.
  • GroupFilter
    The group-related exclusion filter you want to set on the connection. Don't supply this parameter if you don't want to set a group-related exclusion filter.

Get-FilterAttributes
Use this cmdlet to get a list of all available user-related or group-related attributes on a given user profile connection. Each of the returned strings can be used in the Get-FilterSetCondition cmdlet to create a condition on. You can find an example of this in Example3.ps1
Syntax:
Get-FilterAttributes -Connection $connection -User
Get-FilterAttributes -Connection $connection -Group
Parameters:
  • Connection
    A reference to a user profile connection (Microsoft.Office.Server.UserProfiles.Connection object)
  • User
    A switch parameter. Supply this if you want to list all user-related attributes on the connection. Cannot be used in combination with the -Group switch
  • Group
    A switch parameter. Supply this if you want to list all group-related attributes on the connection. Cannot be used in combination with the -User switch

Get-FilterSetConditionOperators
Lists all available operators that you can use in Get-FilterSetCondition

Get-FilterSetOperators
Lists all available operators that you can use in Get-FilterSet


That's brilliant ... but how do I use this in SharePoint's management shell?!
  1. Download either the complete package or the standalone archive. Look in the download section for a link ...
    • The standalone package only contains the necessary assemblies and some example scripts.
    • The complete package also contains the source-code I used, so you could make your own modifications. The source-code is located in the src folder, but I've also copied the necessary assemblies and examples in the distribution folder for easy access
  2. Whatever you downloaded, copy the VNTG.UserProfileFilters.PowerShell.dll and VNTG.UserProfileFilters.dll assemblies to a folder on your SharePoint 2010 server.
  3. Open a new SharePoint 2010 Management Shell on that server
  4. Load the cmdlets using Import-Module ".\VNTG.UserProfileFilters.PowerShell.dll"
  5. That's it! You can now use the cmdlets to create exclusion filters. Don't forget to check out the Example*.ps1 scripts that are also present in the downloaded packages. These scripts provide you with some real-life scenarios to create exclusion filters using the new cmdlets.


Disclaimer and downloads
Like I mentioned in the first part of this post: the workaround is based on reflection, in order to call the internal classes/methods that SharePoint 2010 uses in its Central Admin pages.
I'm sure they're hidden for a reason, so this workaround is definately not officially supported!

USE IT AT YOUR OWN RISK!
Neither I nor my bosses at Ventigrate can be held reliable if something goes wrong!
(The download is hosted at their codeplex project, so they forced me to add this disclaimer ... :p)

If you're interested in the complete source: use this download. This zip-archive contains the source of both the .NET workaround mentioned in the first part, as well as the powershell cmdlets. It also comes with additional examples to clarify its usage.

If you're not interested in any source code, but just want to use this workaround in your SharePoint 2010 Management Shell: use this download. This archive only contains the necessary .NET assemblies and some example ps1 scripts on how to use the cmdlets in your Powershell 2 console.


Examples
I'll list the same example that was mentioned in my original post - only this time using the custom cmdlets. This example is also available as Example1.ps1 in the downloads.
This code will create a user-attribute based exclusion filter and a group-attribute based exclusion filter and assign it to a user profile connection.
  • The user-attrib exclusion filter will filter on "userAccountControl bit on equals 2" or "company equals ventigrate"
  • The group-attrib exclusion filter will filter on "name equals testgroup"
Check the Example2.ps1 script if you want an example that only adds a user-attribute based filter.

#region STEP 1: load the necessary references
#--------------------------------------------
# Load the sharepoint powershell cmdlets
Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue | Out-Null

# Load the custom userprofile cmdlet dll
Import-Module ".\VNTG.UserProfileFilters.PowerShell.dll"
#endregion

#region STEP 2: get the user profile connection on which you want to create exclusion filters
#--------------------------------------------------------------------------------------------
# Get a reference to the service application called 'User Profile Service Application' (very original)
# and a connection called Example 1.
# Change these parameters to whatever user profile service app & connection you want to change on 
# your environment...
$serviceAppName = "User Profile Service Application"
$connectionName = "Example 1"

$profileApp = Get-SPServiceApplication | ? {$_.DisplayName -eq $serviceAppName}
$profileContext = [Microsoft.SharePoint.SPServiceContext]::GetContext(
          $profileApp.ServiceApplicationProxyGroup, 
          [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
          
# Get a new UserProfileConfigManager
$configManager = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager($profileContext)

# Get the connection called "Example 1" in the service application
$connection = $configManager.ConnectionManager | ? {$_.DisplayName -eq $connectionName}

#endregion

#region STEP 3: create the conditions and bind them into an user-filter and a group-filter
#-----------------------------------------------------------------------------------------
# Execute Create-FilterSetCondition to create 2 user-related conditions and 1 group-related condition
# - userCondition1 = userAccountControl bit on equals 2
# - userCondition2 = company equals ventigrate
# - groupCondition = name equals testgroup
$userCondition1 = Get-FilterSetCondition -Connection $connection -ConditionType "user" -Attribute "userAccountControl" -Operator "Bit_on" -Value "2"
$userCondition2 = Get-FilterSetCondition -Connection $connection -ConditionType "user" -Attribute "company" -Operator "Equals" -Value "ventigrate"
$groupCondition = Get-FilterSetCondition -Connection $connection -ConditionType "group" -Attribute "name" -Operator "Equals" -Value "testgroup"

# Combine the user-related conditions in a single user FilterSet, using an OR operand
$userConditions = @($userCondition1, $userCondition2)
$userFilter = Get-FilterSet -FilterType "user" -Operator "or" -Conditions $userConditions

# "Combine" the group condition in another group FilterSet, using an AND operand (though it doesn't matter as it's a single condition ;))
$groupConditions = @($groupCondition)
$groupFilter = Get-FilterSet -FilterType "group" -Operator "and" -Conditions $groupConditions

#endregion

#region STEP 4: assign the filters to the connection
#---------------------------------------------------
# Assign the newly created filters to the connection
Set-ExclusionFilter -Connection $connection -UserFilter $userFilter -GroupFilter $groupFilter

#endregion

I think this code is pretty self-explaining (lots of comments in there).
As always: leave a comment if something isn't 100% clear ... or if you just want to post some constructive feedback :)

Monday 11 July 2011

SharePoint 2010 - Creating user profile exclusion filters using code

Ah, user profiles in SharePoint 2010. When I initially read about them, I was really impressed by the concept: no more SSP, nicely separated service applications, etc. But now that I had the honour to experience them first-hand in some real-life scenarios … well, let’s say they sometimes want me to unleash my inner hulk.

Anyway I digress. In this post I’d like to cover a little black spot in the user-profile API. As you might know, SharePoint 2010 ships with a ****load of PowerShell commands and API functions that allow you to script your entire farm set-up. If you Google around, you’ll find plenty of blogs on how to script various pieces of your SharePoint farm.
Nevertheless there’s something I just can’t seem to find any info on: how to script the creation of user profile connection exclusion filters. There’s even no mention of them in the TechNet resources. Plenty of guides on how to set them up using the UI though, but nothing scriptable.

Setting up filters in the SP2010 UI - Step1

Setting up filters in the SP2010 UI - Step 2
Unfortunately my current employer wants to script everything regarding their farm installation, so I had to get my own hands dirty.

Fire up the disassemblers!
This time I didn’t rely on my old companion Reflector (see my earlier post for more details), but I tried out three other products that attempt to fill the void that Reflector left:
  • ILSpy
  • JetBrains dotPeek
  • Telerik JustDecompile
I’ll probably post a small review on all three later on. Using these babies I loaded up the code-behind from the /_layouts/editconnectionfilters.aspx page, which is used to set the exclusion filters with the UI. Its code behind is the Microsoft.SharePoint.Portal.UserProfiles.AdminUI.EditConnectionFilters class in the Microsoft.SharePoint.Portal assembly.

<insert random curse />

Apparently Microsoft uses internal classes to create these exclusion filters. There goes plan A: call the undocumented public API to create the filters. Also, it doesn’t seem like these internal classes are referenced in any existing SharePoint PowerShell cmdlets, so out went plan B too: call some undocumented cmdlets.

… and in comes plan C
So with all easy options gone, it’s time to bring out the big guns: use reflection to invoke the internal classes in a similar way as it’s done in the UI’s code-behind (of course they just reference the objects without reflection).

Warning: calling internal classes with reflection is certainly not supported in any way! I’m sure Microsoft had their reasons to keep them internal. Or maybe they were just too busy trying to get everything working before the product’s shipping date & forgot to write a public API for these filters. :)
In any case: use the following code at your own risk. I’m not sure if Microsoft will void your warranty for this, but a warned man/woman …
I'm not going to delve too deep into the internal workings of SharePoint and how they handle the creation of exclusion filters ... but briefly summarized they use the following internal classes:
  • FilterSetCondition (located in the Microsoft.Office.Server.UserProfiles.Synchronization assembly)
    This class represents a single “rule” in the filter, e.g. [userAccountControl bit on equals‘2’]
  • FilterSet (also located in the Microsoft.Office.Server.UserProfiles.Synchronization assembly)
    This represents a set of rules (FilterSetCondition objects), combined together using either an “All apply” operator (AND) or an “Any apply” operator (OR) [just like the UI]. User-related rules are combined in one FilterSet, group-related rules are combined in another FilterSet object.
  • Both FilterSet objects are then assigned to the user profile sync connection using the internal SetExclusionFilters method on the connection object.
I'm in a lazy mood today, so I suggest you browse the internal classes yourselves to get a clear view on how it all ties together. (+ I don't want to anger Microsoft too much by pasting bits of their code)


Reflection workaround

You can find the complete source of this workaround on codeplex. The code should also give you some clues on how things are handled internally in SharePoint. The codeplex download consists of two VS2010 projects:
  • a class library project with the actual workaround: VNTG.UserProfileFilters
  • a console application with some examples on calling the class library objects: VNTG.UserProfileFilters.Examples
I tried to properly document everything in that codeplex project, so I hope it's all self-explanatory.
UPDATE: This solution is mainly aimed at Visual Studio 2010 (.NET) projects. If you're looking for a PowerShell-friendly implementation of this workaround, check out part 2 of this post

Once again, use this code at your own risk as it's not officially supported! 
just covering my ass ;)



Just as a teaser I'll also paste one of the examples here. It should give you an idea on how easy it is to add those exclusion filters with the workaround.

Example 1
In this example we’ll add 2 user-based exclusion filters (any apply) and a single group-based exclusion filter to a user profile connection called Local AD:
  • User exclusion filter: ["userAccountControl bit on equals '2'" OR "company equals 'ventigrate'"]
  • Group exclusion filter: ["name equals 'test'"]

// Get a reference to the first available user profile service app
SPFarm localFarm = SPFarm.Local;
var upServiceproxy = SPFarm.Local.Services.Where(s => s.GetType().Name.Contains("UserProfileService")).FirstOrDefault();

if (upServiceproxy != null)
{
 var upServiceApp = upServiceproxy.Applications.OfType<SPIisWebServiceApplication>().FirstOrDefault();
 if (upServiceApp == null)
 {
  Console.WriteLine("No User profile svc app found");
  return;
 }

 SPServiceContext ctx = SPServiceContext.GetContext(upServiceApp.ServiceApplicationProxyGroup, SPSiteSubscriptionIdentifier.Default);
 Microsoft.Office.Server.UserProfiles.UserProfileConfigManager upConfigManager = new Microsoft.Office.Server.UserProfiles.UserProfileConfigManager(ctx);

 // Let's create some user filters for the sync connection called "Local AD"
 var connection = upConfigManager.ConnectionManager["Local AD"];                
 if (connection != null)
 {
  //----------------------------------------------
  // Create a set of user-related rules, any apply
  //----------------------------------------------
  Filters userFilter = new Filters("user");
  var fsCondition1 = userFilter.CreateFilterSetCondition(connection, "userAccountControl", Filters.FilterOperator.Bit_on, "2");
  var fsCondition2 = userFilter.CreateFilterSetCondition(connection, "company", Filters.FilterOperator.Equals, "ventigrate");

  // get the actual filterset based on an OR between all conditions
  List<object> conditions = new List<object>();
  conditions.Add(fsCondition1);
  conditions.Add(fsCondition2);
  var userFilterSet = userFilter.GenerateFilterSetForConditions(conditions, Filters.FilterSetOperator.Or);

  //------------------------------------------------------
  // Create a set of group-related rules, all apply (=AND)
  //------------------------------------------------------
  Filters groupFilter = new Filters("group");
  var groupCondition1 = groupFilter.CreateFilterSetCondition(connection, "name", Filters.FilterOperator.Equals, "test");

  conditions = new List<object>();
  conditions.Add(groupCondition1);
  var groupFilterSet = groupFilter.GenerateFilterSetForConditions(conditions, Filters.FilterSetOperator.And);

  //---------------------------------------------
  // Add the created filtersets to the connection
  //---------------------------------------------
  Filters helper = new Filters("");
  helper.SetExclusionFilters(connection, userFilterSet, groupFilterSet);
 }
}
Example code

Lets dissect this example. First part of the code is to retrieve the user profile connection itself. This is done using the public API of SharePoint 2010 (no reflection at all).
SPFarm localFarm = SPFarm.Local;
var upServiceproxy = SPFarm.Local.Services.Where(s => s.GetType().Name.Contains("UserProfileService")).FirstOrDefault();

if (upServiceproxy != null)
{
 var upServiceApp = upServiceproxy.Applications.OfType<SPIisWebServiceApplication>().FirstOrDefault();
 if (upServiceApp == null)
 {
  Console.WriteLine("No User profile svc app found");
  return;
 }

 SPServiceContext ctx = SPServiceContext.GetContext(upServiceApp.ServiceApplicationProxyGroup, SPSiteSubscriptionIdentifier.Default);
 Microsoft.Office.Server.UserProfiles.UserProfileConfigManager upConfigManager = new Microsoft.Office.Server.UserProfiles.UserProfileConfigManager(ctx);

 // Let's create some user filters for the sync connection called "Local AD"
 var connection = upConfigManager.ConnectionManager["Local AD"];                
 

Then we’ll create the necessary FilterSetCondition objects. This is done with reflection in the CreateFilterSetCondition method of the Filters helper class. You need to specify in the Filters constructor whether you want to build  exclusion rules for user-related attributes (using the "user" parameter) or whether you want to build exclusion rules for group-related attributes (using the "group" parameter).
Finally we group the different user-related and group-related rules together in their own FilterSet objects by calling the GenerateFilterSetForConditions method. In this method you also specify if you want the different rules to be combined as "All apply" (=AND) or as "Any apply" (=OR).

//----------------------------------------------
// Create a set of user-related rules, any apply
//----------------------------------------------
Filters userFilter = new Filters("user");
var fsCondition1 = userFilter.CreateFilterSetCondition(connection, "userAccountControl", Filters.FilterOperator.Bit_on, "2");
var fsCondition2 = userFilter.CreateFilterSetCondition(connection, "company", Filters.FilterOperator.Equals, "ventigrate");

// get the actual filterset based on an OR between all conditions
List<object> conditions = new List<object>();
conditions.Add(fsCondition1);
conditions.Add(fsCondition2);
var userFilterSet = userFilter.GenerateFilterSetForConditions(conditions, Filters.FilterSetOperator.Or);

//------------------------------------------------------
// Create a set of group-related rules, all apply (=AND)
//------------------------------------------------------
Filters groupFilter = new Filters("group");
var groupCondition1 = groupFilter.CreateFilterSetCondition(connection, "name", Filters.FilterOperator.Equals, "test");

conditions = new List<object>();
conditions.Add(groupCondition1);
var groupFilterSet = groupFilter.GenerateFilterSetForConditions(conditions, Filters.FilterSetOperator.And);

Finally, we’ll add both FilterSet objects to the connection that we fetched in the first step.
Filters helper = new Filters("");
helper.SetExclusionFilters(connection, userFilterSet, groupFilterSet);

Result in SharePoint UI

As you can see, the rules provided in the code are now listed on the user profile connection page in the UI. For me this worked like a charm on my dev-machines, I hope it does the same on yours.
Once again: feel free to post a comment if you do have questions on using this...

Download link to the .NET code
Once again: if you're looking for a Powershell-friendly solution -> check my update

[UPDATED: moved the inline code to a codeplex project & rewritten some bits for extra clarity]

Thursday 17 March 2011

"This Site" search scope in an anonymous, publicly available site

It's been a while, but I finally encountered a little SharePoint 2007 issue that wasn't properly solved before on these internets :)

Scenario
The setup consists of a MOSS 2007 publishing site, that is publicly available to anonymous users. The site itself has activated the LockDown feature, preventing anonymous users from viewing SharePoint application & admin pages.
Furthermore, the publishing site has a Search Center set up, which is used to display the search results.
Pretty basic setup if you ask me.

Problem
As you would've guessed by the title, the problem lies in the This Site contextual search scope.

Scope dropdown on default search control

Searches on the custom All Sites scope were no problem, those were displayed in a nice results overview in the search center.
But if an anonymous user launched a query on that contextual This Site: ... scope, he would receive a login-prompt. Not quite the functionality you want to see on a public-facing site.

Cause
The cause has already been mentioned in this post. Apparently the default search control uses 2 different URLs to display its search results:
  • Custom scope queries (the ones you manage in site collection admin pages) are redirected to the search center's results pages.
  • Contextual scope queries (like This Site) are redirected to /_layouts/OSSSearchResults.aspx, regardless of the search center settings.

Unfortunately, thanks to the lockdown feature, the /_layouts/OSSSearchResults.aspx page will be blocked for anonymous users ... resulting in a login prompt when a user launches a query on a contextual scope.

Solution
I wouldn't write this post if there was an easy solution :)
Some blogs have already covered some possible workarounds to this, but I wasn't satisfied with their solutions: some involved modifying out-of-the-box SharePoint pages (blasphemy!) or writing a custom search control from scratch.

I went for a workaround based on .NET reflection.
I admit it, it's not that kosher, but at least this approach left most of the original search box's functionality untouched + it's a lot less work than writing a control from scratch.
The following code will subclass the default searchbox and overwrite the contextual scope's result-page URL with the value set in the public SearchResultPageURL property. This will ensure that any query (on both custom and contextual scope) will be redirected to the same page: the search center's results page.

/// 
/// Customized SearchBox that will override the contextual scope's result page
/// (/_layouts/OSSSearchResults.aspx?k=test&cs=This%20Site&...) with the URL 
/// defined in the SearchResultPageURL property.
/// 
public class SearchBox : SearchBoxEx
{
 protected override void CreateChildControls()
 {
  base.CreateChildControls();

  try
  {
   FieldInfo[] fields = typeof(SearchBoxEx).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
   FieldInfo ossSearchResultField = null;
   int i = 0;
   while (ossSearchResultField == null && i < fields.Length)
   {
    if (fields[i].Name.Equals("m_strOssSearchResultsUrl", StringComparison.InvariantCultureIgnoreCase))
    {
     ossSearchResultField = fields[i];
    }
    i++;
   }

   // Field found -> set its value to the search center's URL
   if (ossSearchResultField != null && !string.IsNullOrEmpty(SearchResultPageURL))
   {
    ossSearchResultField.SetValue(this, SearchResultPageURL);
   }
  }
  catch (Exception ex)
  {
   // Do some logging
  }
 }
}
Just add this control to your masterpage instead of the default search control and you'll be able to use contextual scopes in a public-facing, anonymous SharePoint publishing site without getting login-prompts.
Or just use it if you want to make sure that all search results are displayed using the same results page.
<CustomControls:SearchBox ID="SearchBox" 
  RegisterStyles="false" 
  TextBeforeDropDown="..." 
  TextBeforeTextBox="..."
  TextBoxWidth="100"
  UseSiteDefaults="true" 
  DropDownModeEx="ShowDD" 
  SuppressWebPartChrome="true"
  runat="server" />

Tuesday 1 March 2011

.NET Reflector replacement

All good stories must come to an end ... RedGate's .NET reflector tool is no exception to that rule. I've been addicted to that little gem from the day it was launched, allowing me to find workarounds for all kinds of SharePoint issues by digging into the SharePoint assemblies.
But alas, RedGate has decided to remove the free version & force existing users to buy their tool (even if you're only using an outdated, plugin-free version to browse assemblies ;)) ...

Now I'm not going to turn this post into a rant on the hypocrisy of suddenly charging people for a tool that has been freeware for years. I'm sure they had their reasons for this and their current price of $35 isn't that wallet-shattering either.  Considering the amount of options it gives you, it's still reasonably priced.

Still I'm a cheapskate and I didn't use all of Reflector's advanced options, so I went looking for a free replacement that allowed me to browse the code of existing .NET assemblies.
And behold, it looks like there's a promising alternative: ILSpy!

Overview of its features:
  • Assembly browsing
  • IL Disassembly
  • Decompilation to C#
  • Saving of resources
  • Search for types/methods/properties (substring)
  • Hyperlink-based type/method/property navigation
  • Base/Derived types navigation
  • Navigation history
  • BAML to XAML decompiler
  • Save Assembly as C# Project
What are you waiting for?  Head over to their site and download the latest version ;)

UPDATE
As mentioned by prantlf in the comments, there are even more alternatives:

All of these products are still in beta, so I don't think it's fair that I do a review on them. I've tested them all out and lets say they each have their pros and cons ...
But as they're all free products (for now), I can only suggest that you try them out yourselves and see which one you like best ;)