Navigation

Showing posts with label SharePoint 2010. Show all posts
Showing posts with label SharePoint 2010. Show all posts

Thursday, 17 May 2012

Search Core Results Web Part with Dynamic Date and User Profile Tokens

If you just want the goodies then you can get them here:
The Big Fat Disclaimer - This has not been thoroughly tested for a production environment. I have also removed references in my snippets below to caching and error handling to try and keep it brief. The downloadable version uses both caching and error handling, but it is still really just a proof of concept and you should TEST it before you deploy it! I take no responsibility if your production servers blow up!
I must have seen this requirement dozen of times on different projects, having search results which either:
  • Filter using a User Profile Property of the current user
  • Filter using a dynamic date range (e.g. using the “TODAY” token)
  • Specify the Sort-By (which is normally restricted to either “Relevance” or “Modified Date”
The requirement for functionality of this nature come up extremely frequently on Intranet projects. For example:
“Show News Articles from the past 7 days which filter based on the user’s location”
“Show events coming up in the next 3 months”
“Show discussions / wiki entries / blog posts which include the current user’s Ask Me About values”
Example
image
FixedQuery used in the Web Part:
Author:[UPP-PreferredName] AND Write:[TODAY-180]..[TODAY] AND ContentType:Event
Well .. on my current client project these very requirements came up .. so this time I decided to knock together the basics of the web part in my spare time and then “donate” it to the project… and this post describes how I built it, what goes on under the hood, and also includes both the source code as well as a downloadable WSP package with the working Web Part in it.

Step 1 – Extending the Search Core Results Web Part
So .. to get us started, lets kick off by creating our actual Web Part. I am going to be extending the Search CoreResultsWebPart (link to MSDN).
This is easy enough to achieve by simply creating a new Web Part in Visual Studio and inheriting from the CoreResultsWebPart class. This will make sure our web part gets all of the functionality and properties that the normal Search Results web part does without any additional effort.
   1:  [ToolboxItemAttribute(false)]
   2:  public class ExtendedSearchWebPart : CoreResultsWebPart
   3:  {
   4:   
   5:  }
That is the easy bit …

Step 2 – Overriding the Query and SortOrder
Now the next bit to tackle is how to override the actual query that gets executed. Well the best place to do this is to override the ConfigureDataSourceProperties method. This method gets called before the query is actually executed against the Search engine itself.

You can then leverage the CoreResultsWebPart.DataSource property (which is of type CoreResultsDataSource). This is what allows all of the magic to happen.

   1:  protected override void ConfigureDataSourceProperties()
   2:  {
   3:      // only perform actions when we are trying to show search results
   4:      // i.e. not when you're in Design Mode
   5:      if (this.ShowSearchResults)
   6:      {
   7:          // call the base web part method
   8:          base.ConfigureDataSourceProperties();
   9:   
  10:          // get the data source object
  11:          CoreResultsDatasource dataSource = this.DataSource as CoreResultsDatasource;
  12:   
  13:          // override the query being executed
  14:          dataSource.Query = "Author:\"Martin Hatch\"";
  15:   
  16:          // remove the original sort order
  17:          dataSource.SortOrder.Clear();
  18:          dataSource.SortOrder.Add("Title", Microsoft.Office.Server.Search.Query.SortDirection.Ascending);
  19:      }
  20:  }

So lets talk through the code above.

First off we want to make sure we are only executing our custom code when we are actually trying to retrieve search results. This is a fail-safe block as some instances this will be false (such as when you are editing the web part or if you are viewing the “Design” view in SharePoint Designer). We also need to call the base method (as you typically would when overriding a method call!).

Then things get interesting. Line 11 has us create our “CoreResultsDataSource” object from the local “DataSource” property. This has two properties which we are modifying:

Query (line 14) – This allows us to change or completely override the query which is being executed. This will be the entire query including the Fixed Query, Appended Query and whatever the user typed into their search box (if you are using this on a Search Results page). In my example above I am simply overriding the result so that it just searches for items created by “Martin Hatch” (me!)

SortOrder (lines 17 and 18) – This allows us to override the sort order, allowing us to select ANY indexed Search Property you want (I expect excluding rich text fields of course!). In my example, I am sorting by Title in Ascending order.
This can then be easily extended to provide custom Web Part properties to allow the Sort functionality to be specified by the page editor.

Step 3 – Making it re-usable Part 1 - Dynamic Date ranges
 So now that we can override the query easily we can move on to adding some of the good stuff. I decided to go with a relatively simple Token Replacement function using a simple [TODAY] token to represent the current date:
  • [TODAY] (todays date)
  • [TODAY+7] (today plus 7 days)
  • [TODAY-7] (today minus 7 days)
So .. how do we code this in? Well .. I am quite lazy and don’t really get on with regular expressions (if you are reading this and you are a RegEx guru.. by all means download the source code, refactor it and send it back, cheers!).

So I started off by creating a bunch of class level constants which I would use to recognise the tokens that we are looking for above:

private const string TODAY_PLACEHOLDER = "[TODAY]";
private const string TODAY_ADD_STARTSTRING = "[TODAY+";
private const string TODAY_SUBTRACT_STARTSTRING = "[TODAY-";
private const string TOKEN_ENDSTRING = "]";

The following code can then be swapped out for our ConfigureDataSourceProperties method.

   1:  protected override void ConfigureDataSourceProperties()
   2:  {
   3:      // only perform actions when we are trying to show search results
   4:      // i.e. not when you're in Design Mode
   5:      if (this.ShowSearchResults)
   6:      {
   7:          // call the base web part method
   8:          base.ConfigureDataSourceProperties();
   9:   
  10:          // get the data source object
  11:          CoreResultsDatasource dataSource = this.DataSource as CoreResultsDatasource;
  12:   
  13:          // get the current Fixed Query value from the web part
  14:          string strQuery = this.FixedQuery;
  15:   
  16:          // swap out the exact "today" date
  17:          if (strQuery.IndexOf(TODAY_PLACEHOLDER) != -1)
  18:          {
  19:              strQuery = strQuery.Replace(TODAY_PLACEHOLDER, DateTime.UtcNow.ToShortDateString());
  20:          }
  21:   
  22:          // perform all of the "Add Days" calculations
  23:          while (strQuery.IndexOf(TODAY_ADD_STARTSTRING) != -1)
  24:          {
  25:              strQuery = CalculateQueryDates(strQuery, TODAY_ADD_STARTSTRING, true);
  26:          }
  27:   
  28:          // perform all of the "Remove Days" calculations
  29:          while (strQuery.IndexOf(TODAY_SUBTRACT_STARTSTRING) != -1)
  30:          {
  31:              strQuery = CalculateQueryDates(strQuery, TODAY_SUBTRACT_STARTSTRING, false);
  32:          }
  33:   
  34:          // swap out the Fixed Query for our Calculated Query
  35:          dataSource.Query = dataSource.Query.Replace(this.FixedQuery, strQuery);
  36:      }
  37:  }

This then calls the CalculateQueryDates support method which I put together:
   1:  private static string CalculateQueryDates(string strQuery, string startStringToLookFor, bool AddDays)
   2:  {
   3:      try
   4:      {
   5:          // get the index of the first time this string appears
   6:          int firstIndex = strQuery.IndexOf(startStringToLookFor);
   7:   
   8:          // get the text which appears BEFORE this bit
   9:          string startString = strQuery.Substring(0, firstIndex);
  10:   
  11:          // get the text which appears AFTER this bit
  12:          string trailingString = strQuery.Substring(firstIndex);
  13:          int endIndex = trailingString.IndexOf(TOKEN_ENDSTRING);
  14:          if (endIndex + 1 == trailingString.Length)
  15:          {
  16:              // there is nothing else after this
  17:              trailingString = "";
  18:          }
  19:          else
  20:          {
  21:              trailingString = trailingString.Substring(endIndex +1);
  22:          }
  23:   
  24:          // find the number of days
  25:          string strDays = strQuery.Substring(firstIndex + startStringToLookFor.Length);
  26:          strDays = strDays.Substring(0, strDays.IndexOf(TOKEN_ENDSTRING));
  27:          int days = int.Parse(strDays);
  28:   
  29:          // re-construct the query afterwards
  30:          if (AddDays)
  31:          {
  32:              strQuery = startString + DateTime.UtcNow.AddDays(days).ToShortDateString() + trailingString;
  33:          }
  34:          else
  35:          {
  36:              // subtract days
  37:              strQuery = startString + DateTime.UtcNow.AddDays(0 - days).ToShortDateString() + trailingString;
  38:          }
  39:   
  40:          return strQuery;
  41:      }
  42:      catch (FormatException ex)
  43:      {
  44:          throw new FormatException("The format of the [TODAY] string is invalid", ex);
  45:      }
  46:      catch (ArgumentNullException ex)
  47:      {
  48:          throw new FormatException("The format of the [TODAY] string is invalid. Could not convert the days value to an integer.", ex);
  49:      }
  50:  }

So you should be able to see we are using simple String.IndexOf() method calls to find out if our Tokens are present.

If they are then we simply calculate the DateTime value based on the static DateTime.UtcNow property and use String.Replace() methods to swap out these into our query text.

When we are using [TODAY+X] or [TODAY-X] we simply use DateTime.UtcNow.AddDays(X) or DateTime.UtcNow.AddDays(0-X) and use the same String.Replace() method.

The search syntax is exactly the same as it was previously, and the Keyword Syntax is very powerful.
Example: Using [TODAY] Token query syntax

Write:[TODAY] – this will return all items that were modified today
Write>[TODAY-7] – this will return all items that were modified in the past week
Write:[TODAY-14]..[TODAY-7] – this will return all items that were modified between 2 weeks ago and 1 week ago

So we already have a powerful and reusable search component .. but there is more!

Step 4 – Making it re-usable Part 2 - Dynamic User Profile Properties
The next one is to allow us to pull in User Profile Properties so that we can start doing searches based on the current user’s profile values.
For this we needed to create new replacable Tokens, for which I decided to use:
  • [UPP-{User Profile Property Internal Name}]
  • [UPP-PreferredName] (swaps out for the users name)
  • [UPP-SPS-Responsibility] (swaps out for their “Ask Me About” values)
  • etc ..
So .. we add another class level constant (same as we did for our DateTime tokens)

private const string USER_PROFILE_PROP_STARTSTRING = "[UPP-";

We can then use this in our code, in exactly the way we did before (using String.IndexOf(), String.SubString() and String.Replace() methods).

So we add the following additional code to our ConfigureDataSourceProperties method;
   1:  if (dataSource.Query.IndexOf(USER_PROFILE_PROP_STARTSTRING) != -1 &&
   2:      UserProfileManager.IsAvailable(SPServiceContext.Current))
   3:  {
   4:      string strQuery = dataSource.Query;
   5:   
   6:      while (strQuery.IndexOf(USER_PROFILE_PROP_STARTSTRING) != -1)
   7:      {
   8:          strQuery = ReplaceUserProfilePropertyTokens(strQuery);
   9:      }
  10:   
  11:      if (strQuery != dataSource.Query)
  12:      {
  13:          dataSource.Query = strQuery;
  14:      }
  15:  }

This uses the additional method call ReplaceUserProfilePropertyTokens which is shown below:

   1:  private static string ReplaceUserProfilePropertyTokens(string strQuery)
   2:  {
   3:      // retrieve the current user's Profile
   4:      UserProfileManager upm = new UserProfileManager(SPServiceContext.Current);
   5:      UserProfile profile = upm.GetUserProfile(false);
   6:   
   7:      if (profile == null)
   8:      {
   9:          throw new ApplicationException("The current user does not have a User Profile");
  10:      }
  11:   
  12:      // extract the user profile property name from the token
  13:      int startIndex = strQuery.IndexOf(USER_PROFILE_PROP_STARTSTRING);
  14:      string strPropertyName = strQuery.Substring(startIndex + USER_PROFILE_PROP_STARTSTRING.Length);
  15:      strPropertyName = strPropertyName.Substring(0, strPropertyName.IndexOf(TOKEN_ENDSTRING));
  16:   
  17:      string strToReplace = strQuery.Substring(startIndex);
  18:      strToReplace = strToReplace.Substring(0, strToReplace.IndexOf(TOKEN_ENDSTRING) + 1);
  19:   
  20:      try
  21:      {
  22:          // get the value
  23:          UserProfileValueCollection propertyValue = profile[strPropertyName];
  24:          string strValues = String.Empty;
  25:   
  26:          foreach (object propValue in propertyValue)
  27:          {
  28:              if (propValue.ToString().IndexOf(" ") == -1)
  29:              {
  30:                  strValues += propValue.ToString() + " OR ";
  31:              }
  32:              else
  33:              {
  34:                  strValues += "\"" + propValue.ToString() + "\" OR ";
  35:              }
  36:          }
  37:   
  38:          if (strValues.Length > 0)
  39:          {
  40:              strValues = strValues.Substring(0, strValues.Length - 4);
  41:          }
  42:   
  43:          // swap the value out in the query
  44:          strQuery = strQuery.Replace(strToReplace, strValues);
  45:   
  46:      }
  47:      catch (ArgumentException ex)
  48:      {
  49:          throw new FormatException("The User Profile Property specified in your UPP token does not exist", ex);
  50:      }
  51:      return strQuery;
  52:  }

So there are a few things to point out here which might trip you up:
  • We are using the UserProfileManager.IsAvailable() method to find out if we have a user profile service application provisioned and assigned to the current Web Application.
  • At the moment this code throws an error if the current user doesn’t have a User Profile. You may want to handle this differently for your environment?
  • Handling of multi-value fields. At the moment all we do is take the string values and concatenate them with “OR” in the middle. So if you had “Value1; Value2” as your property value the Token replacement would put “Value1 OR Value2” as the search query.
As long as the content editors are aware of the behaviour this allows us to create quite complex queries.
Example if we now used the Fixed Query:
([UPP-SP-Responsibility]) AND Write:[TODAY-14]..[TODAY]
Then for a user who’s “Ask Me About” properties were “SharePoint” and “IT Administration” then the resulting Search Query would be:
(SharePoint OR “IT Administration”) AND Write:13/05/2012..17/05/2012
If another user comes along whose “Ask Me About” property was just set to “Marketing” then the resulting Search Query would be:
(Marketing) AND Write:13/05/2012..17/05/2012
This is without changing any of the web part properties, and allows us to drive dynamic content from a single web part to our entire user base.

Hopefully you can see that this is incredibly powerful and flexible.

Step 5 – Making it re-usable Part 3 – Controllable Sort By
The final step is to allow our content editors to control the “Sort By” functionality. The default OOTB webpart only allows us to sort by “Relevance” or “Last Modified”, which is fine when you are looking at general search results, but when you are building custom components (such as news, links or event feeds) you typically want to control the order by date or title or something a little more usable for the specific component.

So this bolt-in allows you to control the Sort By. First off we need to add some Web Part Properties so that the user can modify their values:

   1:  [Personalizable(PersonalizationScope.Shared)]
   2:  [WebBrowsable(true)]
   3:  [WebDescription("Sort by this managed property")]
   4:  [WebDisplayName("Managed Property")]
   5:  [Category("Sort Override")]
   6:  public string OrderByProperty { get; set; }
   7:   
   8:  [Personalizable(PersonalizationScope.Shared)]
   9:  [WebBrowsable(true)]
  10:  [WebDescription("Sort direction")]
  11:  [Category("Sort Override")]
  12:  public Microsoft.Office.Server.Search.Query.SortDirection SortDirection { get; set; }

This will provide the Web Part property editing functionality:
image

Once we have done that, we can add the following code to our ConfigureDataSourceProperties method (yes .. this method really is where all of the grunt work goes on in this web part!)
   1:  // if OrderByProperty is not set, use default behavior
   2:  if (!string.IsNullOrEmpty(OrderByProperty))
   3:  {
   4:      // change the sortorder
   5:      dataSource.SortOrder.Clear();
   6:      dataSource.SortOrder.Add(OrderByProperty, SortDirection);
   7:  }

And that is all there is to it.

Step 6 – Enjoy!
So congratulations if you made it this far. I know this was a long blog post but thought it was worth walking through it properly.

If you have any questions, feedback or questions then please get in touch using the comments, and here are links to the downloads (which are also referenced at the top of this blog post)
Some notes about the “final” version:
  • The code structure is slightly different because the DateTime [TODAY] queries are cached using Web Part Properties for better performance
  • the [TODAY] token is case sensitive!
  • There is an extra “Debug Mode” checkbox in the Web Part Properties which when enabled spits out the entire query being executed at the bottom of the search results.
  • Code contains an “Editor Part” .. this just clears out the Cache value when the web part properties are modified
Usage Summary:

Tokens you can use are:
  • [TODAY]
  • [TODAY+X] (add X days)
  • [TODAY-X] (remove X days)
  • [UPP-{Internal Name of User Profile Property}]
Example Usage
Sample user has:
Name: Martin Hatch
Ask Me About: SharePoint; Solution Architecture; Code
ContentType:Event AND ([UPP-SPS-Responsibility]) AND Write:[TODAY-7]..[TODAY]
becomes
ContentType:Event AND (SharePoint OR "Solution Architecture" OR Code) AND Write:12/05/2012..17/05/2012
Returns all events which were updated within the past week, and contain the current user's "Ask Me About" values.

Author:[UPP-PreferredName] IsDocument:1
becomes
Author:"Martin Hatch" IsDocument:1
Returns all documents written by the current user

Author:[UPP-PreferredName] Write>=[TODAY-14]
becomes
Author:"Martin Hatch" Write>=02/05/2012
Returns all content created by the current user and updated within the past 2 weeks


Tuesday, 15 November 2011

My top missing features in Windows Phone 7 - SharePoint Contacts, Tasks and Calendars

I have been an advocate of Windows Phone 7 (WP7) for quite some time now, especially having owned and used a WP7 device for over 9 months now. However, there are some things that I really do find lacking.

Microsoft have been pushing the SharePoint integration with WP7 quite hard, especially with the recent announcements for SharePoint Online to support BCS (which basically opens up the door to have WCF calls made from your Office 365 SharePoint Online site, so you could do two-way WP7 applications which integrate tightly with your SharePoint applications!).

However, there are some features which I think would really concrete this device for small businesses as THE device to have.

Let me give you two example quotes from small businesses who were looking to implement Office 365 / SharePoint Online with a Windows Phone 7;

"Plumbing Business - We would like to use SharePoint task lists to document jobs and push those tasks to our plumbers using their phone.
Our problem is that when the plumber updates the task, we want to it write back to SharePoint.." 
"Pub Landlords - We want to use SharePoint Online to track and store all of our suppliers and contacts that we use, but how do I get those contacts onto my phone? If one of my suppliers calls, how do I know who they are?"
These are just examples of some of the reasons why I think WP7 is missing a trick.

Missing Feature 1 - Use SharePoint Contacts as Phone Book
At the moment there is no way for my WP7 device to use a SharePoint contact list as a phone book. This is a MAJOR piece of functionality I have been asked about by the last 3 clients who were implementing SharePoint Online.

They have all of their contacts centralised and through the web. They can bring them into the desktop Outlook application (with even two-way synchronisation!) but they can't even get them read-only on their phone.

You really should be allowed to "link" one or more SP contact lists with your phone book, so that when someone in your sales team get a phone call the contact details pop-up.

Missing Feature 2 - WP7 overlay with SharePoint Calendar
Pretty similar story here for the calendar. Person is out on site and wants to engage with their {Supplier | Customer | Partner}. They get asked simple requests like "can you book a meeting room for us next week?"

How awesome would it be to be able to bring up a SharePoint Calendar being used for resource bookings, central meetings or events, and overlay it with your own personal calendars (so that you get notifications and can view everyhing in one calendar).

They already do this with Hotmail / Live accounts, so why not SharePoint? This really is a must .. the ability to invite / write to that Calendar from the standard Calendar interface would be a bonus!

Missing Feature 3 - Working with SharePoint Tasks
This is a standard one here .. and really works on so many different levels;
  • I want to see my tasks in a SharePoint List in my WP7 calendar
  • I want to get notifications on my WP7 when a SharePoint task is overdue
  • I want to be able to update the details of a SharePoint task while I am roaming, and people in the office to see those details straight away
If you can wire in the SharePoint workflow / events to the Task list this suddenly becomes very very powerful! You could build entire business centric applications using nothing other than some centrally controlled Task Lists, some Workflow (which you knocked up in SharePoint Designer in a couple of days) and an off-the-shelf WP7 device!

.....

This really is the tip of the iceberg, I could go on and on, but I really think that until this gets fixed the WP7 will continue to be nothing more than a decent consumer device, with little to offer to businesses beyond what other handsets are doing.

Any Android / iPhone / Blackberry (or lets face it .. 10 year old Nokia) can synchronise your Exchange Mailbox ... it is the SharePoint (and other LOB) integration that will make WP7 an "Enterprise" device!!

Monday, 14 November 2011

Summary of SharePoint Saturday UK 2011

Well, this was actually my first SharePoint Saturday experience and I have to say I was massively impressed! The whole day was very well organised and felt like other SharePoint conferences I have been to in the past (with a great variety of the sessions available and excellent quality and depth of the content being presented).

I actually brought a friend with me to SPSUK and he is mostly looking at Office 365 and Windows Phone technologies so that ended up being one of my main focuses as well. I also spent some time prepping (and packing away) from my session, as well as some time in the "Ask the Experts" room (where I met @MossLover and @SharePointBuzz for the first time :)) so I didn't get round to as many sessions as I would have otherwise liked.

Configuring Kerberos in a SharePoint 2010 Farm (#SPSUK06)
I was up first presenting this session and I was very pleased with how it went. We had a great turnout, some really good questions and (to my relief) all of the demos worked really well first time! :) This was a re-run of my SUGUK session in August on the same subject and I'm quite pleased with how the session shaped up.

It was very nice getting people coming up to me during the breaks, in the Ask the Experts session or even on twitter and email afterwards (asking questions, or just telling me how much they enjoyed the session) .. these kind of touch points really make the whole thing worth while :)

If you are looking for my slide decks then you can find them here:
They are branded for SUGUK but the content is the same so you should find everything you need :)


  • Download PowerPoint Slide Deck (PPTX) (zip)
  • View online using PowerPoint Web App

  • (PS - The PowerPoint Web App is powered by Office 365, so hope it works well for you. Feedback welcome!)

    Extending SharePoint 2010 LOB Apps to Windows Phone 7 (#SPSUK23)
    This was a good presentation by Chris Forbes (@chris_e_forbes) on Windows Phone 7 development and SharePoint 2010 integration. This is an area I am getting very interested in for two major reasons:
    1. Office 365 now supports BCS in SharePoint Online, so you can write "no-code" methods of calling WCF web services (which potentially allows the Windows Phone 7 "push notification" services which Microsoft host)
    2. The new "Mango" (Windows Phone 7.5) release includes back-ground tasks, which may allow a background application to respond to push notifications and execute custom code.
    This really opens up the doors in terms of having a very powerful zero-infrastructure solution leveraging both Office 365 and Windows Phone 7!

    Sort your processes with easy, effective InfoPath Forms and SharePoint Workflows (#SPSUK15)
    My final session of the day was with Ian Woodgate (@ianwoodgate) and ran through some cool InfoPath techniques (easy cascading drop-downs) and especially the InfoPath "Approval" mechanism which is being championed by Laura Rogers (@WonderLaura).

    Everytime I look at InfoPath I get more and more impressed, and with Office 365 it really does open up a lot of doors in terms of process automation, workflow and external communications without having to write any custom code (which is ideal when, in SharePoint Online, your development is limited to the SharePoint 2010 Sandbox which restricts a lot of methods).

    Steve Fox - “SharePoint and the Cloud: Crash or Convergence?”
    The end of the day was spent with Steve Fox (@redmondhockey) from Microsoft giving us some live demos of the new Windows Azure platform and some SharePoint 2010 integration (both on-premise and using SharePoint Online) as well as Windows Phone 7.

    Wrap Up
    This was a really good day. I was quite surprised at the number of people there (for a free event, all day on a Saturday) and everyone had a very relaxed non-commercial attitude to the day which was refreshing for a "conference" type event.

    I will definately be going to the next one .. and I seriously recommend that you do too!

    To sum up the day I'll quote from my friend (@Denyerec)

    Back from , or by its other name . Very worthwhile day out.


    Tuesday, 4 October 2011

    Scaling to 10,000 unique permissions - Part 2 - The Solution

    This follows on from my previous post; Part 1 - The Problem

    The main requirement was:

    • One SharePoint 2010 site
    • 10,000+ uniquely permissioned objects each with a different user account

    In this post we will be discussing the solution which involves programmatically creating unique permissions in a way which will scale for (what should be) well over 10,000 uniquely permissioned items...

    Introducing yet another little known SharePoint API call ...

    This is only possible because of one of the new SharePoint 2010 API calls;

    SPRoleAssignmentCollection.AddToCurrentScopeOnly()

    http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.sproleassignmentcollection.addtocurrentscopeonly.aspx

    This basically adds the specified SPRoleAssignment but does not create any of the Limited Access scopes on the parent objects.

    This is pretty straight forward and works in exactly the same way to normal Role Assignments in SharePoint 2010, we simply use the AddToCurrentScopeOnly() method instead of using the Add() method, for example:

     

       1: // fetch the Principal object which we are granting access to
       2: SPUser user = web.EnsureUser("Domain\\UserAccount");
       3:  
       4: // create a Role Assignment binding
       5: SPRoleAssignment roleAssignment = new SPRoleAssignment(user);
       6:  
       7: // apply contribute permissions
       8: roleAssignment.RoleDefinitionBindings.Add(
       9:     web.RoleDefinitions["Contribute"]);
      10:  
      11: // grant permissions to the list item using the CURRENT SCOPE ONLY
      12: // this ensures that Limited Access scopes are NOT created
      13: // for parent objects (we're going to have to do that bit ourselves!)
      14: item.RoleAssignments.AddToCurrentScopeOnly(roleAssignment)

     

    It is very important to understand that you still need to grant "Limited Access" (it wasn't put in just for laughs, it does have a purpose). Granting "Limited Access" means that the object has access to core information on parent objects to enable construction of things like the breadcrumb, and retrieval of core files needed to render the interface.

    This then means it is up to us (the developers) to go back and create each of those in a more efficient way. The problem is .. you can't assign "Limited Access" programmatically...

    What do you mean .. I can't assign Limited Access??

    Well, I don't really know why they did this, but if you try and assign it programmatically (Limited Access is actually a "Permission Level" in SharePoint) you will get errors (admittedly you can't do this through the user interface either!).

    So, the workaround (again) is to create your own permission level which includes exactly the same permissions that "Limited Access" would have granted. This is:


    • View Application Pages
    • Browse User Information
    • Use Remote Interfaces
    • Use Client Integration Features
    • Open

    You can call this anything you like (I called mine "SP Limited Access") as long as you know what it means.

    The code to do this is as follows:

     


       1: internal SPRoleDefinition GetLimitedAccessRole(SPWeb web)
       2:         {
       3:             string strRoleDefinition = "SP Limited Access";
       4:  
       5:             // only exists in webs with unique role definitions
       6:             if (web.HasUniqueRoleDefinitions)
       7:             {
       8:                 try
       9:                 {
      10:                     // try to retrieve the role definition
      11:                     return web.RoleDefinitions[strRoleDefinition];
      12:                 }
      13:                 catch (SPException)
      14:                 {
      15:                     // SPException means it does not exist
      16:  
      17:                     // create our custom limited access role
      18:                     SPRoleDefinition roleDef = new SPRoleDefinition();
      19:  
      20:                     // give it a name and description
      21:                     roleDef.Name = "SP Limited Access";
      22:                     roleDef.Description = "Identical to standard " + 
      23:                         "Limited Access rights. " + 
      24:                         "Used to provide access to parent objects of " + 
      25:                         "uniquely permissioned content";
      26:  
      27:                     // apply the base permissions required
      28:                     roleDef.BasePermissions = SPBasePermissions.ViewFormPages 
      29:                         | SPBasePermissions.Open 
      30:                         | SPBasePermissions.BrowseUserInfo 
      31:                         | SPBasePermissions.UseClientIntegration 
      32:                         | SPBasePermissions.UseRemoteAPIs;
      33:  
      34:                     // add it to the web
      35:                     web.RoleDefinitions.Add(roleDef);
      36:                 }
      37:  
      38:                 return web.RoleDefinitions[strRoleDefinition];
      39:             }
      40:             else
      41:             {
      42:                 // try the parent web
      43:                 return GetLimitedAccessRole(web.ParentWeb);
      44:             }
      45:         }

    I've created my new Limited Access Permission Level .. now what?

    One thing does need to be made clear, there is absolutely no point you just creating all of the Security Scopes that SharePoint would have created (you'll end up with the same mess we were trying to avoid in the first place).

    The solution is to create a group for all of the "Limited Access" users for that List or Web. It really is up to you whether you use Active Directory Security Groups or SharePoint Groups. I decided to use AD security groups; mainly because I didn't want to clog up the Site Collection "groups" functionality, and didn't want idiot Site Collection admins from removing the group members (or worse .. the groups themselves!) and breaking the site collection.


    Note - I haven't included the code to create and modify Active Directory Security Groups here, if nothing else because there are thousands of resources out there showing you how to modify AD groups programmatically, and Code Project has a particularly good reference: Howto: (Almost) Everything In Active Directory via C#


    You will need to create a group for each parent object which has unique permissions although in my example it is only really the SPWeb (web site) that we are worried about as the libraries and folders are well within the security scope threshold.

    So we have our 20 libraries and our root web site. So in our example we would have to create 21 different AD security groups:


    • One group to store all Limited Access users for the root web site

    • 20 groups to store all Limited Access for the libraries (one for each library)

    Then, following this example you can then use the following code to grant “Limited Access” to one of the libraries (and just rinse and repeat for the other libraries and the root web site);

     


       1: // fetch the "SP Limited Access" role definition
       2: SPRoleDefinition limitedAccessRole = GetLimitedAccessRole(web);
       3:  
       4: // get SPPrincipal object for the AD Group we created
       5: SPUser adGroup = web.EnsureUser("My Custom AD Group Name");
       6:  
       7: // set the role assignments for this group
       8: SPRoleAssignment roleAssignment = new SPRoleAssignment(adGroup);
       9: roleAssignment.RoleDefinitionBindings.Add(limitedAccessRole);
      10:  
      11: // grant "Limited Access" to the AD Group for this list
      12: // we only have to do this once! After this we simply 
      13: // need to add members to this AD Group every time we 
      14: // add users to one of the parent objects!
      15: list.RoleAssignments.AddToCurrentScopeOnly(roleAssignment)

    So having done this for all of the parent objects we now have our 21 custom Active Directory groups, each one of which has been granted “Limited Access” to one of the required “parent” objects for our folders.

    From here on in it should be smooth sailing. You simply need to make sure that every time you programmatically add a new user to one of the folders you also make sure they get added to the relevant AD Groups (so that the “Limited Access” chain is not broken).

    The following diagram really explains what we have done:

    Folders_New

    I have tested this model for over 16,000 unique AD accounts across hundreds of folders in hundreds of document libraries and I cannot notice any discernable drop off in performance (nothing that can’t be explained by simply having a really large number of libraries and folders anyway!) so initial tests show that this is working very well indeed :)

    What I also ended up doing (to make this slightly more robust) is to build my own application page which users can use to Grant Permissions through the UI (so we don’t need to write custom code every time a new “Limited Access” scope is needed).

    I then wrote an HttpModule to auto-redirect any requests to the out-of-the-box page (_layouts/AclInv.aspx) to the custom page so that if anyone tried to use the native user interface it would ALWAYS be executing my own custom code (which creates all of the AD Groups and SP Limited Access scopes programmatically, without the user having to worry about it!)

    The great thing about this solution is that it doesn't matter how many users or groups you are adding to your SharePoint site .. you only ever have 1 Limited Access security scope for each List / Web!



    Thanks for sticking with me through these two posts .. if you made it this far then thanks for reading and I would love to hear your comments! :)