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 ;)

Sunday, 22 August 2010

How to create a BCS lookup field in a content type

Hellloooooo fellow frustrated SharePoint devs!
(Well I assume you are, otherwise you wouldn't be interested in this ...)

I'm currently working on a little project/proof-of-concept which involves heavy usage of the new SharePoint 2010 BCS functionalities. I've already come across a few bumps in the road for which I couldn't immediately find some useful info, so I guess this will be the first of many posts on this subject.

Today I'll start with the following issue: how to use a BCS lookup field in a content type.
Normally this shouldn't be to hard to do:
  1. Create a site column "A"
  2. Create a content type (on site collection level) "CT"
  3. Add a reference to column "A" in content type "CT"
However, no matter how hard I looked, I just couldn't find an option to create a BCS lookup field as site column. Fyi: a BCS lookup field is called External Data field in the UI.
Sure, you can easily create such field on a list level, but not on a site level.


Adding a list BCS lookup. Notice the External Data type


Trying to add a site BCS lookup. No external data type here ... :(



So after googling around without any result, I found it was time to fumble around with my trusty Powershell and see how these list fields are defined.
Apparently the field's SchemaXml is:
<field
type="BusinessData"
displayname="bcs_field"
required="FALSE"
enforceuniquevalues="FALSE"
id="{GUID}"
sourceid="{GUID}"
staticname="bcs_field"
baserenderingtype="Text"
name="bcs_field"
colname="nvarchar11"
rowordinal="0"
version="3"
group=""
systeminstance="SPSDEV1"
entitynamespace="SPSDEV1.BCS"
entityname="dbo_Persons"
bdcfield="Names"
profile=""
hasactions="True"
addfieldoption="AddToAllContentTypes, AddFieldToDefaultView">
</field>
Notable properties are:
  • SystemInstanceName: this is the BCS instance name.
    You can find this value via Central Admin > Application Management > Manage Service Applications > Business Data Connectivity Services. Then select External Systems and select the system's name. This should result in an URL like http://centraladmin/_admin/BDC/ViewBDCLobSystemInstances.aspx?AppId=...

  • EntityNamespace: BCS entity's namespace
  • EntityName: BCS entity you're referencing to
  • BdcFieldName: the BCS field you want to display in the column
Those last three properties' values can be found in the External Content Types overview of Central Admin > Application Management > Manage Service Applications > Business Data Connectivity Services





Armed with this info, I cooked up this piece of code which will create a BCS lookup site column. Of course, you can then reuse this column in any content type.
private string CreateBCSLookupField(SPWeb targetWeb,
string lookupFieldName, string groupName,
string systemInstanceName, string entityNamespace,
string entityName, string entityFieldName, bool hasActions)
{
SPBusinessDataField lookupField =
targetWeb.Fields.CreateNewField("BusinessData", lookupFieldName) as SPBusinessDataField;
lookupField.Group = groupName;
lookupField.SystemInstanceName = systemInstanceName;
lookupField.EntityNamespace = entityNamespace;
lookupField.EntityName = entityName;
lookupField.HasActions = hasActions;
lookupField.BdcFieldName = entityFieldName;
return targetWeb.Fields.Add(lookupField);
} 

*UPDATE*
Okay, apparently there's still something fishy going on behind the scene. I've been using this code for a while now and it seems there's an inconsistency when you create multiple BCS site columns in a row.
Lets say you create 3 columns (A, B, C) with this code.
  • First test:
    A was ok, B too, but C resulted in an error about an improperly configured lookup.
  • Second test (same code, same setup):
    A was ok, B impropertly configged and C was ok
  • Third test (again same setup):
    A failed, B was ok and C failed too
To be honest, I really don't know why it sometimes fails.  It's the same frikking code, but somehow SharePoint sometimes decides to funk things up.
Funking things up apparently comes down to bad property values.  I compared the schema xml's of properly created BCS fields and those that weren't: the correct fields had the same XML as I posted before, while the erronous fields had this xml
<Field  
Type="BusinessData"  
DisplayName="bcs_field"  
Required="FALSE"  
EnforceUniqueValues="FALSE"  
Group="BCS Test"  
ID="{GUID}"  
SourceID="{GUID}"  
StaticName="bcs_field"  
Name="bcs_field"  
BaseRenderingType="Text"  
ColName="nvarchar11"  
RowOrdinal="0" />
As you can see, this xml doesn't have the BCS related properties ... but why?!
Currently I only have one "workaround":
  1. Create BCS field
  2. Check if the resulting fields schema xml contains a reference to SystemInstance (or another BCS related property)
  3. If it doesn't, remove the field and try again.  Usually it'll then be created properly
  4. Rince and repeat if it still doesn't have a correct schema xml (of course, limit the number of repeats to prevent endless loops)
I know, it's definately not kosher ... but I just don't have a clue at the moment on what's causing these missing properties.
Any ideas are welcome :)

Wednesday, 11 August 2010

Querying SharePoint 2010 SPSite from within console application results in FileNotFoundException

Tried to connect to a SharePoint 2010 sitecollection via a console application today, but it kept failing with an FileNotFoundException.
Code:

using(SPSite site = new SPSite("http://sps2010"){
//FileNotFoundException
}

Just for future reference:
  1. make sure your application is using the .NET 3.5 framework
  2. ensure it's compiled as 64 bit

As you'll have guessed by now: my app was using the visual studio 2010 default settings of .NET framework 4.0, compiled as x86 ... which obviously conflicts with the Microsoft.SharePoint assemblies (that are built on 3.5 and as 64bit.) :)

Monday, 28 June 2010

Removing SSL from SharePoint Central Administration

Encountered yet another SharePoint 2007 inconsistency today.

Situation:
One of my customers wanted to test out an SSL-secured central admin page to allow for SSL-secured content deployments. His wish is my command (he's paying the bills after all), so I proceeded with executing the following stsadm command
stsadm -o setadminport -port 443 -ssl

As expected, after clicking on the Sharepoint Central Admin link, we were greeted by a nice https://server****/ url in our browser's address bar. Huzzah!
As can be read on many other blogs covering the setup of SSL in central admin, I then proceeded with installing the SSL certificates to finalize the setup. 'T was a true miracle, but the SSL central admin actually worked.

However, due to some problems with the SSL certificate itself (not the setup), the content deployment tests failed. So we ended up in removing the SSL from the central admin again.
Should be easy stuff, right?

WRONG! Remember, we're dealing with SharePoint after all ;)
My first guess was: "let's run the stsadm again but without the -ssl parameter"
stsadm -o setadminport -port 10000

This resulted in a nice "Operation completed succesfully" message, but when we clicked on the central admin link ... we were greeted by a less nice https://server***:10000. So, looks like the command succesfully changed the portnumber, but unfortunately it kept the https part.

Tried some other things, but they all failed. The portnumber changed, but the https part didn't move an inch.
So it was time to bring out the big guns:
  • .NET Reflector (awesome tool!)
  • Powershell (even more awesome for these situations!)
Ran a reflector on the stsadm.exe command, after some digging I found the code responsible for the setadminport operation:
Microsoft.SharePoint.Administration.SPGlobalAdmin.SetAdminPort()

(not going to post the code here, don't wanna risk lawsuits ... if you're after the code: disassemble it yourself ;))


In general, this method will open the SPAdministrationWebApplication.Local object, change its ServerBindings or SecureBindings properties (based on the -ssl flag), modify the AlternateUrls SPAlternateUrlCollection and finally create a webapp provisioning job.

Armed with that information, I started fiddling around in Powershell. Remember kids, only fiddle around in a VM with a recent snapshot ... don't do this on production servers! You'll find out why in a couple of paragraphs :)

First thing I noticed: after executing the "stsadm -o setadminport -port 10000", I ended up with 2 alternate url's in the AlternateUrls property for the webapp's default zone. Looks like we're getting a bit warmer:
  • one with https://server*:10000 as incoming url
  • one with http://server*:10000 as incoming url
Attempt 1:
Tried to manually delete the https alternate url by calling AlternateUrls.Delete(0). However, calling Update() on the AlternateUrls property will then result in dependency errors and a failing central admin.
There was no way to fix this, so I ended up in restoring a VM snapshot :)


Attempt 2:
Instead of removing the urls, I tried calling the SetResponseUrl method on the AlternateUrls property & passed the http://server*:10000 as parameter.
Guess what: this method replaced both alternate url definitions with a single alternate url (which I had supplied in the method parameters). Result!

For all you lazy arses out there, I used the following Powershell script to solve it. Maybe it's easier to read than my ranting :)
//Load the SharePoint assembly
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")

//Get the central admin webapp
$cAdmApp = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]
::Local

//Create a new alternate url
$altUrl = New-Object "Microsoft.SharePoint.Administration.SPAlternateUrl"
-ArgumentList "http://server:10000", "Default"

//Set this alternate url as response url
$cAdmApp.AlternateUrls.SetResponseUrl( $altUrl )
$cAdmApp.AlternateUrls.Update()

//Get the alternate urls definition, this should only list our
//http://server:10000 url
$cAdmApp.AlternateUrls
... and there was much rejoicing.