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]

1 comment:

  1. Wow thanks so much Tom, you helped me track down that elusive Microsoft.Office.Server.UserProfiles.Synchronization.dll reference!! Much appreciated all the way from Canada :)

    ReplyDelete