Second Life of a Hungarian SharePoint Geek

July 26, 2016

How to Invoke PSI Methods using PSContext, Demonstrated by an Example of Reading Delegation Information

Filed under: PS 2013, PSI, Reflection — Tags: , , — Peter Holpar @ 21:21

The Challange

So you may ask now, why would we like to access PSI functionality from PSContext at all? Let me explain our situation.

Recently we had to extend an existing server side code in our custom Project Server solution to enable delegate users (that means ones acting for another users at the given time) to access the same custom functionality as the user they acting for.

Note: You can manage delegation via PWA Settings / Manage Delegates (either in the Personal Setting or in the Security section), see the page http://YourProjectServer/PWA/_layouts/15/pwa/userdelegation/ManageDelegations.aspx?mgm=true.

To make the things event worse, we had to check the delegation information in code running in the SharePoint web context, and in code that runs as a simple server side process without web context. It is actually not a timer job, but an automated server process scheduled by Windows Task Scheduler for the sake of simplicity, that perform some kind of housekeeping and reporting tasks for the custom application.However, in the case of a time job, an asynchronous event receiver, or a workflow step running without web context, you would have the same issue. Why these two cases (with or without SharePoint web context) differ from each other, will be discussed later in more details.

You probably know, that although the server side object model of the Project Server (available via the Microsoft.ProjectServer.PSContext) covers quite a lot of the functionality of the PSI, there is still a plenty of features that are simply inaccessible via the objects available through PSContext. For example, there is no way to access the delegation information. That means for us, that we should read this information via PSI, as its Resource class provides a ReadDelegations method just for this purpose. If you are not new in Project Server development, you should have already your own experience with PSI. If you have not any experience with that yet, I can say you, that it is rather complex comparing to the server side OM provided by PSContext. You should work with various DataSet and DataTable objects, but the greatest problem in our case that it requires a lot of configuration, either by config files (see the Configuring the Services with app.config section of the Walkthrough: Developing PSI Applications Using WCF article for example) , or via code (see section 11 of  the Walkthrough: Developing PSI Applications Using WCF article, or the Configuring the Services Programmatically section of the Walkthrough: Developing PSI Applications Using WCF article for example), just to be able to start using the PSI web services. If you have more applications (in our case one with, and another one without SharePoint web context), multiple environments (development, test, and production, one with HTTP, another one with HTTPS), and multiple servers in the environments, creating and maintaining the configuration information is typically not a trivial task.

Why could not we simply access the PSI endpoints via the PSContext? It would be great, wouldn’t it? Are you surprised if I say, it is possible? Well, actually it is. It requires an amount of hacking, and probably not a supported solution, but it is technically possible, and it made our life easier. If you take the risk on your own, you can try it as well, however there is no guarantee, that it works for you, or that once it works, a new Project Server patch could not break the functionality. So be warned.

After this introduction you should have already an overview of the problem and hopefully you are ready to read about the technical stuff. If you are not interested in such details, and need only the result of the analysis, you can skip the next two sections.

Connection between the Project Server client side object model and the server side object model

If one dig into the assemblies implementing the server side object model (Microsoft.Project.dll), and the client side object model (Microsoft.Project.Client.dll) of Project Server, then it turns out, that Microsoft has not re-implemented the functionality of PSI, these libraries still utilize the good-old PSI infrastructure either directly (in case of the server side OM) or indirectly (in case of the client side OM, that calls PSI via the server side OM).

How the client side OM invokes the server side OM is not Project Server specific, the infrastructure is inherited from SharePoint. Although there were changes between the SharePoint versions 2010 and 2013, the main concepts remain the same. Both the server side and client side of that bridge were deeply researched and analyzed in my posts five years ago. Those posts should give you enough insight into the internal functionality of the client side OM, this topic is outside of scope of the current post.

It is more exciting (at least, for now), how the Project Server server side OM invokes the PSI infrastructure. Exactly that will I describe in the next section.

Connection between the Project Server server side object model and PSI

The Microsoft.Office.Project.PWA namespace contains a public class called PSI (implemented in assembly Microsoft.Office.Project.Server.PWA). This class exposes more than 20 public properties with postfix “WebService”, each of them returns the WCF proxy of the corresponding PSI channel. For example, the ResourceWebService property returns a reference for a Resource object (namespace: Microsoft.Office.Project.Server.WebServiceProxy, assembly: Microsoft.Office.Project.Server.PWA), inherited from WcfProxyBase<IResource>. Once created in the “WebService” property getters, these WCF proxy objects are stored in the private field _proxyContainer (of  type ProxyContainer, implemented as a nested class) of the PSI class for any further access.

For the purpose of our solution, it is irrelevant how the WCF proxy objects (like the Resource object returned by the ResourceWebService property of the PSI class) know, what configuration they should use and how they can access the WCF endpoint. Probably I write about sometimes later in an other post.

The internal PJClientCallableContext class (Microsoft.ProjectServer namespace, Microsoft.ProjectServer assembly) contains a static read-only property called PJContext. It returns an instance of the PJContext class (Microsoft.Office.Project.PWA namespace, Microsoft.Office.Project.Server.PWA assembly) using the generic private static RetrieveValue method. If the process runs in the HTTP context (HttpContext.Current is not null) the RetrieveValue method invokes the GetContext(bool isWebServiceCall, bool ignoreCachedContext) method of the PJContext class. Otherwise (without HTTP context), the lazy-initialized value of the _pjcontext field will be returned by RetrieveValue method. The lazy-initialization of this field can be found in the Initialize method of the PJClientCallableContext class, the static GetObjectModelContext method of the PJContext class is invoked using the SPWeb object instance passed to the Initialize method as parameter. This is the same SPWeb object, that you passed to the constructor of the PSContext object (or it is the RootWeb of the site if you used the PSContext constructor with the SPSite object; or the RootWeb of the site corresponding to the URL if you used the PSContext constructor with the Uri object), as in the PSContext(SPWeb web) constructor an instance of the PJClientCallableContext class is created using the web instance as parameter.

Side note: On the other hand, using the PSContext constructors with parameters in a HTTP context may result you do no get the expected outcome, but it is beyond our scope again, more on that in a later post.

After this theoretical explanation let’s see some functional code.

The Code

My goal was to access the static PJContext property of the internal PJClientCallableContext class via Reflection, and expose its PSI property to my code as an extension method.

The solution described below requires adding the following assembly references:

  • Microsoft.Project.dll
  • Microsoft.Office.Project.Server.Library.dll
  • Microsoft.Office.Project.Server.PWA.dll
  • Microsoft.Office.Project.Schema.dll
  • Microsoft.Office.Project.Server.Administration.dll

The extension method is rather simple, and requires this using directive to work:

using Microsoft.Office.Project.PWA;

  1. public static PSI GetPSI(this PSContext dummyContext)
  2. {
  3.     PSI result = null;
  4.     Assembly psAssembly = typeof(PSContext).Assembly;
  5.  
  6.     // get internal type
  7.     Type type_PJClientCallableContext = psAssembly.GetType("Microsoft.ProjectServer.PJClientCallableContext");
  8.     PropertyInfo pi_PJContext = type_PJClientCallableContext.GetProperty("PJContext");
  9.     PJContext pjContext = pi_PJContext.GetValue(null) as PJContext;
  10.  
  11.     if (pjContext != null)
  12.     {
  13.         result = pjContext.PSI;
  14.     }
  15.  
  16.     return result;
  17. }

Note: As you can see, in the code above we don’t use the PSContext object passed as parameter to the extension method at all. It is only to enable attaching the functionality to a PSContext instance via the extension method, and so enforcing the creation of a PSContext instance first. If you have an application without HTTP context (like a console application), and you call this code without creating a PSContext instance first, like:

PSI psi = Extensions.GetPSI(null);

an InvalidOperationException will be thrown by the RetrieveValue method of the PJClientCallableContext class:

You must wrap your server OM code in a PJClientCallableContext when outside of an HttpContext.

In the case of a web application you don’t have such problem, as the PJContext instance is created using the current HTTP context.

Below I illustrate the usage of the extension method, by querying the delegation information of an enterprise resource in Project Server. To achieve that, I invoke the ReadDelegations method of the Resource class. See the available values of the DelegationFilter enumeration in the documentation.

The code requires the following using directives:

using Microsoft.Project;
using Microsoft.Office.Project.PWA;
using schema = Microsoft.Office.Project.Server.Schema;
using lib = Microsoft.Office.Project.Server.Library;

  1. using (PSContext projectContext = PSContext.GetContext(new Uri(pwaUrl)))
  2. {
  3.     PSI psi = projectContext.GetPSI();
  4.                     
  5.     lib.UserDelegationConsts.DelegationFilter filter = lib.UserDelegationConsts.DelegationFilter.All;
  6.  
  7.     // replace the GUID with an ID of a resource in your environment
  8.     // whose delegation information you would like to display
  9.     Guid resId = Guid.Parse("087ade95-281e-e411-9568-005056b45654");
  10.  
  11.     using (schema.UserDelegationDataSet delegationDS = psi.ResourceWebService.ReadDelegations(filter, resId))
  12.     {
  13.         foreach (schema.UserDelegationDataSet.ResourceDelegationsRow resDelegation in delegationDS.ResourceDelegations)
  14.         {
  15.             Console.WriteLine("Resource is substituted by '{0}' from '{1}' to '{2}'", resDelegation.DELEGATE_NAME, resDelegation.DELEGATION_START, resDelegation.DELEGATION_FINISH);
  16.         }
  17.     }
  18. }

Using the approach above you don’t have to bother with the configuration of the PSI proxy you need in your application, and still access such features in your server side code, that are available in PSI.

Note: There is a major restriction using this method. Although the PSI web services are available from a client computer, or from a remote server as well, you can not access remote servers using the code above. You can access only the PSI resources exposed by the local farm.

Advertisements

April 18, 2016

Re-creating the Missing Out-of-the-Box Search Locations using PowerShell

Filed under: PowerShell, Reflection, Search, SP 2013 — Tags: , , , — Peter Holpar @ 22:40

Recently we migrated a SharePoint 2010 web application into an existing SharePoint 2013 farm to free up the old SharePoint 2010 farm for “recycling” (it was the last application running in the old farm). The web application has a single site collection, and contains a single business application with views and list forms having a lot of customizations via “jQuery magic”. Since a replacement of the business application is planed for the near future, we decided not to upgrade the site collection to the SharePoint 2013 user interface (version 15). Leaving it in the SharePoint 2010 mode (version 14) ensures the views and forms are working further without any modifications in the JavaScript codes. After a few days a user complained, that when searching the local web site an error is displayed instead of the search results on the _layouts/OSSSearchResults.aspx page:

Unable to display this Web Part. To troubleshoot the problem, open this Web page in a Microsoft SharePoint Foundation-compatible HTML editor such as Microsoft SharePoint Designer. If the problem persists, contact your Web server administrator

In the ULS logs we found these entries:

CoreResultsWebpart: Couldnt find location with internal name LocalSearchIndex
CoreResultsDatasourceView: Couldnt find location with internal name LocalSearchIndex

On the web we found a post from Sushant Dukhande with the description of the issue, and a suggestion for the solution.

Using the script on that site it turned out, that the LocationConfigurations property of the search proxy is really empty. In an other environment, where we tested the migration we had no such issue.

Sushant Dukhande suggests to re-provision the search application. It might really solve the problem, however in our case I felt it to be an intense change, and searched for an alternative solution. Having a look into what happens under the cover of a provisioning process, I found the method responsible for provisioning the missing search locations. It is the internal static CreateOOBLocations method of the Microsoft.Office.Server.Search.Administration.LocationFactory class (in the Microsoft.Office.Server.Search assembly).

First, we need a reference to the search service application. You can get it like this (assuming it is named "Search Service Application"):

$ssa = Get-SPEnterpriseSearchServiceApplication "Search Service Application"

or via this script (as long as you are sure, you have a single instance of this service application type in your farm):

[Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa = Get-SPServiceApplication | ? { $_.TypeName -eq "Search Service Application" }

To display the names of the existing search locations:

$locConfigs = $ssa.LocationConfigurations
$locConfigs | % { $_.InternalName }

The following PowerShell script shows how to invoke the CreateOOBLocations method passing the search service application as parameter using PowerShell and Reflection:

$searchAssembly = [Microsoft.Office.Server.Search.Administration.SearchServiceApplication].Assembly
$locationFactory_Type = $searchAssembly.GetType("Microsoft.Office.Server.Search.Administration.LocationFactory")

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Static
$mi_CreateOOBLocations = $locationFactory_Type.GetMethod("CreateOOBLocations", $bindingFlags)
$mi_CreateOOBLocations.Invoke($null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))

Invoking the CreateOOBLocations method might be not always the solution for you. The same is true for the re-provisioning process suggested by Sushant Dukhande, since it invokes the same method as well. The problem, that this method has a condition, before provisioning all of the default search locations:

if (searchApp.LocationConfigurations.Count < 1)
{
    LocationConfigurationCollection locationConfigurationsInternal = searchApp.GetLocationConfigurationsInternal(true);
    CreateLiveLocation(locationConfigurationsInternal);
    CreateLiveSuggestionsLocation(locationConfigurationsInternal);
    CreateLocalSharepointLocation(locationConfigurationsInternal);
    CreateLocalPeopleLocation(locationConfigurationsInternal);
    CreateLocalFSSharePointLocation(locationConfigurationsInternal);
}

I don’t see, how our farm “lost” its search locations, but if it is possible to “lose” only a subset of the search locations (for example, only the one called LocalSearchIndex), it won’t be re-created by the CreateOOBLocations method, as the count of search location is still not zero.

In this case, the solution may be to re-create only the missing search location via the corresponding method. In the case of the LocalSearchIndex search location it is the CreateLocalSharepointLocation method of the LocationFactory class:

$locConfigs = $ssa.LocationConfigurations
$mi_CreateLocalSharepointLocation = $locationFactory_Type.GetMethod("CreateLocalSharepointLocation", $bindingFlags)
$mi_CreateLocalSharepointLocation.Invoke($null, @([Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection]$locConfigs))

After fixing the issue in the farm, I’ve tested our other farms as well to find out, whether they are affected by the same problem or not. In one of the farm, the script provided in the post I mentioned earlier detected the issue, although I was sure, there is no problem with the search. It turned out to be a false positive test. This farm has its search service as a shared service from another farm, and the user account the script was run with had no permission on the search service in that remote farm. The script simply hid away the access denied error.

However, if we create a LocationConfigurationCollection instance via its internal constructor (either with a parameter of type SearchServiceApplication or of type SearchServiceApplicationProxy), the access denied error is displayed in the case the user has no permissions, and the items of the collection can be accessed if there is no problem with the permissions.

Let’s see first the script using the SearchServiceApplication:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]), $null)
$locConfigs = $ci_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))
$locConfigs | % { $_.InternalName }

As I wrote, you can achieve the same via a service proxy. It is useful for example, if the application is connected to a shared search service of another farm. First, we get the proxy as:

$url = "http://YourSharePointApp/&quot;
$site = Get-SPSite $url
$serviceContext = [Microsoft.SharePoint.SPServiceContext]::GetContext($site)
$ssaAppProxy = $serviceContext.GetDefaultProxy([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy])

Next, we can use the same script as earlier, but in this case we invoke the internal constructor having the SearchServiceApplicationProxy parameter type:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci2_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]), $null)
$locConfigs = $ci2_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]$ssaAppProxy))
$locConfigs | % { $_.InternalName }

November 24, 2015

Recovering Passwords for SharePoint Managed Accounts

Filed under: PowerShell, Reflection, Security, SP 2013 — Tags: , , , — Peter Holpar @ 23:47

We have a SharePoint 2013 farm on a Windows 2008 R2 server. Recently we found this error in the Windows event logs in relation with the web application pool account:

Event ID 1511 – Windows cannot find the local profile and is logging you on with a temporary profile. Changes you make to this profile will be lost when you log off.

We tried to solve the issue based on the information we found in this post, but at the step below we faced the problem, that the password stored for the web application pool account (in this case we assumed domain\wa_pool_account) in our password repository does not work any more.

runas /u:domain\wa_pool_account /profile cmd

The web application pool account is registered as a managed account in SharePoint, at the original password has been already automatically changed by the system.

We could reset the password for the managed account as described in this article, but before changing the password I wanted to be sure there is no way to recover the current password from the system. I found a blog post and the related PowerShell code in TechNet Gallery, but I found the method described there (creating a new web application, and using an external tool, appcmd.exe) a bit overkill.

Instead of this I came up with an alternative solution that query the password directly from the SPManagedAccount object, via its private m_Password field (of type SPEncryptedString) that we can access by using Reflection. The public SecureStringValue property of the SPEncryptedString class returns an instance of the System.Security.SecureString class, and as illustrated here, we can “decode” its value to a simple string via Marshaling.

Using this approach, recovering the managed account password is so simple:

$ma = Get-SPManagedAccount domain\wa_pool_account
$maType = $ma.GetType()

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance

$m_Password = $maType.GetField("m_Password", $bindingFlags)
$pwdEnc = $m_Password.GetValue($ma)

$ssv = $pwdEnc.SecureStringValue
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($ssv)
[System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)

July 2, 2015

Sending mails from a test SharePoint system, Updated for SharePoint 2013

Filed under: Mails, Reflection, SP 2013 — Tags: , , — Peter Holpar @ 23:48

Last year I published a post about how to redirect mails sent from your SharePoint application to a list, storing the mails as attachments for the list items. Unfortunately, the Reflection-based approach described there seems to no longer work in SharePoint 2013. My goal with the current post is to provide an alternative solution to the problem.

The first issue is with the signature of the private Send method of the MailMessage class we used to save the content of the mail into a MemoryStream: it has been extended with an extra bool parameter (allowUnicode) since the .NET  4.5 version. This problem could be easily fixed by altering the method invocation with the extra parameter, as:

sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true, true }, null);

If you test the code in a simple console application, it works, the mail message will be saved into the MemoryStream. However, if you try to use the same code from a SharePoint application (an application referencing the server side SharePoint assemblies: either a console application or web part), you become a nasty exception:

An unhandled exception of type ‘System.AccessViolationException’ occurred
Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

After a short research, it turned out that the reason is the x64 process the SharePoint server side assemblies require. Since we can not change that requirement, I had to find an alternative solution. It was easy, as the stackoverflow thread the original solution originate from already contains an other option that plays with the DeliveryMethod of the SmtpClient class, saving the mail into a file in a temporary folder in the file system. Based on that answer I’ve altered the code of my DummyMailSender.

First, I’ve defined the helper class TemporaryDirectory to clean up the rest after the work:

  1. internal class TemporaryDirectory : IDisposable
  2. {
  3.     public TemporaryDirectory()
  4.     {
  5.         DirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
  6.         Directory.CreateDirectory(DirectoryPath);
  7.     }
  8.  
  9.     public string DirectoryPath { get; private set; }
  10.  
  11.     public void Dispose()
  12.     {
  13.         if (Directory.Exists(DirectoryPath))
  14.         {
  15.             Directory.Delete(DirectoryPath, true);
  16.         }
  17.     }
  18. }

and another class Extensions that implements an extension method to save the mail into a byte array:

  1. internal static class Extensions
  2. {
  3.     public static byte[] GetAsByteArray(this MailMessage m)
  4.     {
  5.         byte[] messageData = null;
  6.  
  7.         var smtpClient = new SmtpClient { DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory };
  8.  
  9.         using (var tempDir = new TemporaryDirectory())
  10.         {
  11.             smtpClient.PickupDirectoryLocation = tempDir.DirectoryPath;
  12.             smtpClient.Send(m);
  13.             var emlFile = Directory.GetFiles(smtpClient.PickupDirectoryLocation).FirstOrDefault();
  14.             if (emlFile != null)
  15.             {
  16.                 // read all file contents and trim the carriage return / new line at the end
  17.                 var messageAsText = File.ReadAllText(emlFile).Trim();
  18.                 System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
  19.                 messageData = enc.GetBytes(messageAsText);
  20.             }
  21.         }
  22.         return messageData;
  23.     }
  24. }

Having these classes that perform the bulk of the functionality, the DummyMailSender can be simplified as:

  1. public void Send(MailMessage message)
  2. {
  3.     Trace.TraceInformation("Mail (subject: '{0}') sending started via DummyMailSender", message.Subject);
  4.  
  5.     byte[] messageData = message.GetAsByteArray();
  6.  
  7.     // RootWeb mustn't be disposed, see:
  8.     // http://blogs.msdn.com/b/rogerla/archive/2008/10/04/updated-spsite-rootweb-dispose-guidance.aspx
  9.     SPWeb rootWeb = _site.RootWeb;
  10.     bool allowUnsafeOriginal = rootWeb.AllowUnsafeUpdates;
  11.     try
  12.     {
  13.         rootWeb.AllowUnsafeUpdates = true;
  14.         SPList mailList = rootWeb.Lists[Constants.DummyMailList];
  15.         SPListItem mailItem = mailList.AddItem();
  16.         mailItem[SPBuiltInFieldId.Title] = message.Subject;
  17.         mailItem.Attachments.Add("Mail.eml", messageData);
  18.         mailItem.Update();
  19.  
  20.         string mailUrl = string.Format("{0}{1}?ID={2}", _site.Url, mailList.DefaultDisplayFormUrl, mailItem.ID);
  21.         Trace.TraceInformation("Dummy mail with subject '{0}' \"sent\" to '{1}' (cc: '{2}'), saved to '{3}'", message.Subject, message.To, message.CC, mailUrl);
  22.     }
  23.     catch (Exception ex)
  24.     {
  25.         Trace.TraceError("Error sending dummy mail: {0}\r\n{1}", ex.Message, ex.StackTrace);
  26.     }
  27.     finally
  28.     {
  29.         rootWeb.AllowUnsafeUpdates = allowUnsafeOriginal;
  30.     }
  31. }

This new version of DummyMailSender might perform not so well, as the original, Reflection-based one, but it should not be a major issue in a test system it was planned for, and at least it is a supported solution. At least, you don’t have to worry about that it won’t work after a newer .NET Framework version.

June 25, 2014

How to restrict the properties of a single object in the results of Client Object Model requests sent from PowerShell on the server side

Filed under: Managed Client OM, PowerShell, Reflection, SP 2013 — Tags: , , , — Peter Holpar @ 21:49

In my recent posts I illustrated how can we restrict (either using expression trees or via dynamically compiled assemblies) on the server side the properties of entity collections returned by the client object model requests when using PowerShell. Although it has less effect on the network traffic, it may be interesting to see, how can we achieve a similar result when working with single objects (like a single list instance) instead of entity collections (for example, fields of a list).

A query without property restrictions might look like this in C#:

ClientContext ctx = new ClientContext("http://sp2013&quot;);
var list = ctx.Web.Lists.GetByTitle("Images");
ctx.Load(list);
ctx.ExecuteQuery();

The request contains no property restriction (see SelectAllProperties=true below),

image

and the response contains indeed dozens of properties.

image

In C# it is quite simple to specify using lambda expressions, which properties of the list we need:

ClientContext ctx = new ClientContext("http://sp2013&quot;);
var list = ctx.Web.Lists.GetByTitle("Images");
ctx.Load(list, l => l.Title, l => l.ItemCount);
ctx.ExecuteQuery();

The request will include the limitation,

image

and only the requested properties are returned in the response:

image

But how to achieve the same result from PowerShell?

Again, we can easily refactor the code with the lambda expressions into a static helper class:

public static class QueryHelper
{
    public static void LoadListWithLimtedFields(ClientContext ctx, List list)
    {
        ctx.Load(list, l => l.Title, l => l.ItemCount);
    }
}

In this case, the original method contains only the code below that is easy to translate to PowerShell:

ClientContext ctx = new ClientContext("http://sp2013&quot;);
var list = ctx.Web.Lists.GetByTitle("Images");
QueryHelper.LoadListWithLimtedFields(ctx, list);
ctx.ExecuteQuery();

The same functionality transformed to PowerShell is shown below:

  1. $url = "http://sp2013&quot;
  2.  
  3. $referencedAssemblies = (
  4.     "Microsoft.SharePoint.Client, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
  5.     "Microsoft.SharePoint.Client.Runtime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
  6.     "System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
  7. $sourceCode = @"
  8. using Microsoft.SharePoint.Client;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11.  
  12. public static class QueryHelper
  13. {
  14.     public static void LoadListWithLimtedFields(ClientContext ctx, List list)
  15.     {
  16.         ctx.Load(list, l => l.Title, l => l.ItemCount);
  17.     }
  18. }
  19. "@
  20.  
  21. Add-Type -ReferencedAssemblies $referencedAssemblies -TypeDefinition $sourceCode -Language CSharp;
  22. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
  23. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
  24.  
  25. $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
  26. $list = $ctx.Web.Lists.GetByTitle("Images")
  27. [QueryHelper]::LoadListWithLimtedFields($ctx, $list)
  28. $ctx.ExecuteQuery()
  29.  
  30. Write-Host $list.Title, $list.ItemCount

Although more complex as the former solution, and recommended only for the hard-core developers, we can achieve the same result using expression trees built using Reflection. Let’s see how Visual Studio compiles the lambda expressions in our C# code into plain .NET objects and method calls. We have to build the code and open the assembly with a decompiler tool.

The original code as decompiled by JetBrains dotPeek:

ClientContext clientContext = new ClientContext("http://sp2013&quot;);
List byTitle = clientContext.Web.Lists.GetByTitle("Images");
clientContext.Load<List>(byTitle, new Expression<Func<List, object>>[2]
{
  (Expression<Func<List, object>>) (l => l.Title),
  (Expression<Func<List, object>>) (l => (object) l.ItemCount)
});
clientContext.ExecuteQuery();

The original code as decompiled by Reflector:

ParameterExpression CS$0$0001;
ClientContext ctx = new ClientContext("http://sp2013&quot;);
List list = ctx.Web.Lists.GetByTitle("Images");
ctx.Load<List>(list, new Expression<Func<List, object>>[] { Expression.Lambda<Func<List, object>>(Expression.Property(CS$0$0001 = Expression.Parameter(typeof(List), "l"), (MethodInfo) methodof(List.get_Title)), new ParameterExpression[] { CS$0$0001 }), Expression.Lambda<Func<List, object>>(Expression.Convert(Expression.Property(CS$0$0001 = Expression.Parameter(typeof(List), "l"), (MethodInfo) methodof(List.get_ItemCount)), typeof(object)), new ParameterExpression[] { CS$0$0001 }) });
ctx.ExecuteQuery();

The decompiled code helps us to create the same expression tree from PowerShell using Reflection:

  1. $url = "http://sp2013&quot;
  2.  
  3. # load the required client object model assemblies
  4. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
  5. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
  6.  
  7. $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
  8. $list = $ctx.Web.Lists.GetByTitle("Images")
  9.  
  10. $expressionType = [System.Linq.Expressions.Expression]
  11. $parameterExpressionArrayType = [System.Linq.Expressions.ParameterExpression].MakeArrayType()
  12.  
  13. $lambdaMethod = $expressionType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2  -and $_.GetParameters()[1].ParameterType -eq $parameterExpressionArrayType }
  14. $lambdaMethodGeneric = $lambdaMethod.MakeGenericMethod([System.Func“2[Microsoft.SharePoint.Client.List,System.Object]])
  15.  
  16. $listType = [Microsoft.SharePoint.Client.List]
  17.  
  18. # query the Title propery of type System.String
  19. $param1 = [System.Linq.Expressions.Expression]::Parameter($listType, "l")
  20. $name1 = [System.Linq.Expressions.Expression]::Property($param1, "Title")
  21. $expression1 = $lambdaMethodGeneric.Invoke($Null, [System.Object[]] @($name1, [System.Linq.Expressions.ParameterExpression[]] @($param1)))
  22.  
  23. # query the ItemCount of type System.Int32
  24. $param2 = [System.Linq.Expressions.Expression]::Parameter($listType, "l")
  25. $name2 = [System.Linq.Expressions.Expression]::Property($param2, "ItemCount")
  26.  
  27. # convert the Int32 type to object
  28. $body2 = [System.Linq.Expressions.Expression]::Convert($name2, [System.Object])
  29.  
  30. $expression2 = $lambdaMethodGeneric.Invoke($Null, [System.Object[]] @($body2, [System.Linq.Expressions.ParameterExpression[]] @($param2)))
  31.  
  32. $loadMethod = [Microsoft.SharePoint.Client.ClientRuntimeContext].GetMethod("Load")
  33. $loadMethodGeneric = $loadMethod.MakeGenericMethod([Microsoft.SharePoint.Client.List])
  34.  
  35. # call Load with both of the expressions
  36. $loadMethodGeneric.Invoke($ctx, [System.Object[]] @($list, [System.Linq.Expressions.Expression`1[System.Func“2[Microsoft.SharePoint.Client.List,System.Object]][]] @($expression1, $expression2)))
  37.  
  38. $ctx.ExecuteQuery()
  39.  
  40. Write-Host $list.Title, $list.ItemCount

Using the methods described in this and in the former posts we can achieve the same network traffic limiting results from PowerShell as we got used to in the case of the C# code.

June 15, 2014

How to restrict the properties in the results of Client Object Model requests sent from PowerShell on the server side, implementing the Include method

Filed under: Managed Client OM, PowerShell, Reflection, SP 2013 — Tags: , , , — Peter Holpar @ 22:51

In my recent post I illustrated how one can limit the entities returned by the server when responding a Client Object Model request sent from PowerShell. Although that method restricts the entities to the ones we really need, it still returns all of the properties for those items.

See the SelectAllProperties = true attribute of the ChildItemQuery in the request captured by Fiddler:

image

And the response includes really all of the properties…

image

However, we need typically only a few properties, the others only increase the size of the response package sent by the server unnecessarily.

In the C# syntax we can limit the fields using the Include method (see Retrieving only specified properties of lists):

  1. ClientContext ctx = new ClientContext("http://sp2013&quot;);
  2. var list = ctx.Web.Lists.GetByTitle("Images");
  3. var fieldsQuery = list.Fields.Include(f => f.InternalName, f => f.Id);
  4. // var fieldsQuery = list.Fields;
  5. var fields = ctx.LoadQuery(fieldsQuery);
  6. ctx.ExecuteQuery();

Remark: The JavaScript version supports a kind of Include as well (see Retrieving Only Specified Properties of Lists Using JavaScript).

Following the same strategy as in the case of the Where method, we compile our code to an assembly, and decompile it using JetBrains dotPeek:

  1. ClientContext clientContext = new ClientContext("http://sp2013&quot;);
  2. IQueryable<Field> clientObjects = ClientObjectQueryableExtension.Include<Field>((IQueryable<Field>)clientContext.Web.Lists.GetByTitle("Images").Fields, new Expression<Func<Field, object>>[2]
  3.   {
  4.     (Expression<Func<Field, object>>) (f => f.InternalName),
  5.     (Expression<Func<Field, object>>) (f => (object) f.Id)
  6.   });
  7. clientContext.LoadQuery<Field>(clientObjects);
  8. clientContext.ExecuteQuery();

Decompiling the code with Reflector reveals the internals of the lambda expression query:

IQueryable<Field> fieldsQuery = ctx.Web.Lists.GetByTitle("Images").Fields.Include<Field>(new Expression<Func<Field, object>>[] { Expression.Lambda<Func<Field, object>>(Expression.Property(CS$0$0001 = Expression.Parameter(typeof(Field), "f"), (MethodInfo) methodof(Field.get_InternalName)), new ParameterExpression[] { CS$0$0001 }), Expression.Lambda<Func<Field, object>>(Expression.Convert(Expression.Property(CS$0$0001 = Expression.Parameter(typeof(Field), "f"), (MethodInfo) methodof(Field.get_Id)), typeof(object)), new ParameterExpression[] { CS$0$0001 }) });

Transferring this quite complicate expression to a bit more readable format our method looks like this:

  1. ClientContext ctx = new ClientContext("http://sp2013&quot;);
  2. var list = ctx.Web.Lists.GetByTitle("Images");
  3.  
  4. // query the InternalName propery of type System.String
  5. var param1 = Expression.Parameter(typeof(Field), "f");
  6. var name1 = Expression.Property(param1, "InternalName");
  7.  
  8. var expression1 = Expression.Lambda<Func<Field, object>>(name1, param1);
  9.  
  10. // query the Id of type System.Guid
  11. var param2 = Expression.Parameter(typeof(Field), "f");
  12. var name2 = Expression.Property(param2, "Id");
  13. // convert the Guid type to object
  14. var body2 = Expression.Convert(name2, typeof(object));
  15.  
  16. var expression2 = Expression.Lambda<Func<Field, object>>(body2, param2);
  17.  
  18. // call Include with both of the expressions
  19. var fieldsQuery = ClientObjectQueryableExtension.Include<Field>(list.Fields, expression1, expression2);
  20.  
  21. var fields = ctx.LoadQuery(fieldsQuery);
  22. ctx.ExecuteQuery();

The PowerShell version of the same functionality:

  1. $url = "http://sp2013&quot;
  2.  
  3. # load the required client object model assemblies
  4. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
  5. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
  6.  
  7. $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
  8. $list = $ctx.Web.Lists.GetByTitle("Images")
  9.  
  10. $expressionType = [System.Linq.Expressions.Expression]
  11. $parameterExpressionArrayType = [System.Linq.Expressions.ParameterExpression].MakeArrayType()
  12.  
  13. $lambdaMethod = $expressionType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2  -and $_.GetParameters()[1].ParameterType -eq $parameterExpressionArrayType }
  14. $lambdaMethodGeneric = $lambdaMethod.MakeGenericMethod([System.Func“2[Microsoft.SharePoint.Client.Field,System.Object]])
  15.  
  16. $fieldType = [Microsoft.SharePoint.Client.Field]
  17.  
  18. # query the InternalName propery of type System.String
  19. $param1 = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  20. $name1 = [System.Linq.Expressions.Expression]::Property($param1, "InternalName")
  21. $expression1 = $lambdaMethodGeneric.Invoke($Null, [System.Object[]] @($name1, [System.Linq.Expressions.ParameterExpression[]] @($param1)))
  22.  
  23. # query the Id of type System.Guid
  24. $param2 = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  25. $name2 = [System.Linq.Expressions.Expression]::Property($param2, "Id")
  26.  
  27. # convert the Guid type to object
  28. $body2 = [System.Linq.Expressions.Expression]::Convert($name2, [System.Object])
  29.  
  30. $expression2 = $lambdaMethodGeneric.Invoke($Null, [System.Object[]] @($body2, [System.Linq.Expressions.ParameterExpression[]] @($param2)))
  31.  
  32. $includeMethod = [Microsoft.SharePoint.Client.ClientObjectQueryableExtension].GetMethod("Include")
  33. $includeMethodGeneric = $includeMethod.MakeGenericMethod([Microsoft.SharePoint.Client.Field])
  34.  
  35. # call Include with both of the expressions
  36. $fieldsQuery = $includeMethodGeneric.Invoke($Null, [System.Object[]] @($list.Fields, [System.Linq.Expressions.Expression`1[System.Func“2[Microsoft.SharePoint.Client.Field,System.Object]][]] @($expression1, $expression2)))
  37.  
  38. $fields = $ctx.LoadQuery($fieldsQuery)
  39. $ctx.ExecuteQuery()
  40.  
  41. $fields | % { Write-Host $_.InternalName: $_.Id }

Testing the script we found in Fiddler, that the request is limited to the properties we need:

image

and the response contains only the requested properties as well:

image

The output of the script should be similar to this one:

image

Note: In the case above, the child items are not restricted via a QueryableExpression, however you can combine the Where method described in the former post and the Include method to restrict both the child entities and their properties returned by the server:

fields.Where(f => f.TypeAsString == "Guid").Include(f => f.Id, f => f.InternalName)

In the PowerShell equivalent we chain our expression to achieve the same result:

  1. $url = "http://sp2013&quot;
  2.  
  3. # load the required client object model assemblies
  4. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
  5. Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
  6.  
  7. $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
  8. $list = $ctx.Web.Lists.GetByTitle("Images")
  9.  
  10. $expressionType = [System.Linq.Expressions.Expression]
  11. $parameterExpressionArrayType = [System.Linq.Expressions.ParameterExpression].MakeArrayType()
  12.  
  13. $lambdaMethod = $expressionType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2  -and $_.GetParameters()[1].ParameterType -eq $parameterExpressionArrayType }
  14.  
  15. $fieldType = [Microsoft.SharePoint.Client.Field]
  16.  
  17. # call the Where method first
  18. $lambdaMethodGenericBool = $lambdaMethod.MakeGenericMethod([System.Func“2[Microsoft.SharePoint.Client.Field,System.Boolean]])
  19.  
  20. $param = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  21. $name = [System.Linq.Expressions.Expression]::PropertyOrField($param, "TypeAsString")
  22. $body = [System.Linq.Expressions.Expression]::Equal($name, [System.Linq.Expressions.Expression]::Constant("Guid"))
  23. $expression = $lambdaMethodGenericBool.Invoke($Null, [System.Object[]] @($body, [System.Linq.Expressions.ParameterExpression[]] @($param)))
  24.  
  25. $whereMethod = [System.Linq.Queryable].GetMethods() | ? { $_.Name -eq "Where" -and $_.ToString() -like "*TSource,System.Boolean*" }
  26. $whereMethodGeneric = $whereMethod.MakeGenericMethod([Microsoft.SharePoint.Client.Field])
  27.  
  28. $fieldsQuery = $whereMethodGeneric.Invoke($Null, [System.Object[]] @($list.Fields, $expression))
  29.  
  30. # call the Include method next on the result of the Where method
  31. $lambdaMethodGenericObj = $lambdaMethod.MakeGenericMethod([System.Func“2[Microsoft.SharePoint.Client.Field,System.Object]])
  32.  
  33. # query the InternalName propery of type System.String
  34. $param1 = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  35. $name1 = [System.Linq.Expressions.Expression]::Property($param1, "InternalName")
  36. $expression1 = $lambdaMethodGenericObj.Invoke($Null, [System.Object[]] @($name1, [System.Linq.Expressions.ParameterExpression[]] @($param1)))
  37.  
  38. # query the Id of type System.Guid
  39. $param2 = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  40. $name2 = [System.Linq.Expressions.Expression]::Property($param2, "Id")
  41.  
  42. # convert the Guid type to object
  43. $body2 = [System.Linq.Expressions.Expression]::Convert($name2, [System.Object])
  44.  
  45. $expression2 = $lambdaMethodGenericObj.Invoke($Null, [System.Object[]] @($body2, [System.Linq.Expressions.ParameterExpression[]] @($param2)))
  46.  
  47. $includeMethod = [Microsoft.SharePoint.Client.ClientObjectQueryableExtension].GetMethod("Include")
  48. $includeMethodGeneric = $includeMethod.MakeGenericMethod([Microsoft.SharePoint.Client.Field])
  49.  
  50. # call Include with both of the expressions
  51. $fieldsQuery2 = $includeMethodGeneric.Invoke($Null, [System.Object[]] @($fieldsQuery, [System.Linq.Expressions.Expression`1[System.Func“2[Microsoft.SharePoint.Client.Field,System.Object]][]] @($expression1, $expression2)))
  52.  
  53. $fields = $ctx.LoadQuery($fieldsQuery2)
  54. $ctx.ExecuteQuery()
  55.  
  56. $fields | % { Write-Host $_.InternalName: $_.Id }

When checking the network traffic using Fiddler we found that both the request

image

and the response fulfills our expectations.

The output of the script contains only the two returned fields due to the filtering on the server side:

image

How to filter the results of Client Object Model requests sent from PowerShell on the server side, implementing the Where method

Filed under: Managed Client OM, PowerShell, Reflection, SP 2013 — Tags: , , , — Peter Holpar @ 00:31

There are cases when we have to use the Managed Client Object Model from PowerShell. One of the most well-known cases is when we would like to manage our Office 365 environment (or access data stored in it) from our PowerShell scripts.

The Managed Client Object Model itself supports the server side filtering of data requested from the client using the Where method (see Applying filters to list retrieval), in contrary to the ECMAScript Client Object Model, where it is possible only via an unsupported workaround.

What it makes easy to use from advanced .NET programming languages like C# are the concepts of static methods, LINQ and lambda expression, all of them is quite far away from the standard PowerShell samples. No surprise, that the examples we can find on the web do not apply this filtering technique at all. If you have a look at this example, you can see, that it first downloads the whole list of site columns over the wire, then filters them on the client side just to get the two columns we need. If we have hundreds of columns, it can be pretty an overkill.

Note: Yes, I know that REST makes it possible (via the $filter query option) to filter the items returned, however it has its own limitation, like the lack of the batch requests – e.g. aggregating requests on the client side and sending them in batches via executeQuery – that is (at least, IMHO) one of the best features of the Client Object Model. Furthermore, I feel REST does not fit so good to the concept of PowerShell as the usage of the Managed Client Object Model.

Since we are able to use the same .NET libraries from PowerShell as from C#, it should be possible to achieve the same filtering result as well. But how to start?

First, let’s see a simple C# sample of a Managed Client Object Model call. In this sample we get the list of those fields of the Images list, that are of type Guid.

  1. ClientContext ctx = new ClientContext("http://sp2013&quot;);
  2. var list = ctx.Web.Lists.GetByTitle("Images");
  3. var fieldsQuery = list.Fields.Where(f => f.TypeAsString == "Guid");
  4. var fields = ctx.LoadQuery(fieldsQuery);
  5. ctx.ExecuteQuery();

How could we translate this one to a version that (although maybe less readable for us, humans) better reflects the object model and method calls behind the syntax sugar of C# (like static methods, and so on)? If we compile our code into an assembly, and open that assembly with a decompiler tool, like JetBrains dotPeek, the result may help us a step further:

  1. ClientContext clientContext = new ClientContext("http://sp2013&quot;);
  2. IQueryable<Field> clientObjects = Queryable.Where<Field>((IQueryable<Field>)clientContext.Web.Lists.GetByTitle("Images").Fields, (Expression<Func<Field, bool>>)(f => f.TypeAsString == "Guid"));
  3. clientContext.LoadQuery<Field>(clientObjects);
  4. clientContext.ExecuteQuery();

That seems already far better, but what can we do with the lambda expression?

Opening the assembly with another decompiler tool, Reflector reveals further details about the internal working of this functionality:

IQueryable<Field> fieldsQuery = Queryable.Where<Field>(ctx.Web.Lists.GetByTitle("Images").Fields, Expression.Lambda<Func<Field, bool>>(Expression.Equal(Expression.Property(CS$0$0000 = Expression.Parameter(typeof(Field), "f"), (MethodInfo) methodof(Field.get_TypeAsString)), Expression.Constant("Guid", typeof(string)), false, (MethodInfo) methodof(string.op_Equality)), new ParameterExpression[] { CS$0$0000 }));

As this decompiled code suggest, and as learned from this forum answer, we can substitute the lambda expression with an equivalent expression tree built dynamically.

The version we receive after this transformation:

  1. ClientContext ctx = new ClientContext("http://sp2013&quot;);
  2. var list = ctx.Web.Lists.GetByTitle("Images");
  3.  
  4. var param = Expression.Parameter(typeof(Field), "f");
  5. var name = Expression.PropertyOrField(param, "TypeAsString");
  6. var body = Expression.Equal(name, Expression.Constant("Guid"));
  7.  
  8. var expression = Expression.Lambda<Func<Field, bool>>(body, param);
  9. var fieldsQuery = Queryable.Where<Field>(list.Fields, expression);
  10.  
  11. var fields = ctx.LoadQuery(fieldsQuery);
  12. ctx.ExecuteQuery();

All we now have yet to do is to translate the C# code to PowerShell. The translation is not extremely complicated, it is mainly a mechanical process, although includes a lot of Type objects and Reflection calls, and we need a few tricks as well to achieve our goal.

For example, in the code above we use the static generic Lambda method of the Expression class with parameter types Expression and ParameterExpression[]. If we liked to get this method by name and parameter types directly using the GetMethod method of the Type class, we would receive an “Ambiguous match found” exception. The cause is that the Expression class has two methods with the same name: one generic one, and a non-generic version, and the GetMethod method does not support to restrict the filter to only one of these. Probably we can get the right one using the FindMembers method as well, but I found it easier to use the GetMethods method to get all of the methods of the Expression class and filter the results in my PowerShell code. In the code below we filter the methods, first by name, next we restrict the results to generic methods, and take the override that has two parameters, and the second parameter is of type ParameterExpression[]:

$lambdaMethod = $expressionType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2  -and $_.GetParameters()[1].ParameterType -eq $parameterExpressionArrayType }

I had similar problems with the Where method of the Query class. Although this method has only two overrides, and these ones have different parameter signatures, the parameters are rather complicated generic parameters, like Expression<Func<TSource, int, bool>>, so I decided to filter the methods again from my code, taking the methods called Where, and selecting the one that has a textural representation with the right parameter types:

$whereMethod = [System.Linq.Queryable].GetMethods() | ? { $_.Name -eq "Where" -and $_.ToString() -like "*TSource,System.Boolean*" }

The result of the translation is:

  1. $url = "http://sp2013&quot;
  2.  
  3. $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
  4. $list = $ctx.Web.Lists.GetByTitle("Images")
  5.  
  6. $expressionType = [System.Linq.Expressions.Expression]
  7. $parameterExpressionArrayType = [System.Linq.Expressions.ParameterExpression].MakeArrayType()
  8.  
  9. $lambdaMethod = $expressionType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2  -and $_.GetParameters()[1].ParameterType -eq $parameterExpressionArrayType }
  10. $lambdaMethodGeneric = $lambdaMethod.MakeGenericMethod([System.Func“2[Microsoft.SharePoint.Client.Field,System.Boolean]])
  11.  
  12. $fieldType = [Microsoft.SharePoint.Client.Field]
  13. $param = [System.Linq.Expressions.Expression]::Parameter($fieldType, "f")
  14. $name = [System.Linq.Expressions.Expression]::PropertyOrField($param, "TypeAsString")
  15. $body = [System.Linq.Expressions.Expression]::Equal($name, [System.Linq.Expressions.Expression]::Constant("Guid"))
  16. $expression = $lambdaMethodGeneric.Invoke($Null, [System.Object[]] @($body, [System.Linq.Expressions.ParameterExpression[]] @($param)))
  17.  
  18. $whereMethod = [System.Linq.Queryable].GetMethods() | ? { $_.Name -eq "Where" -and $_.ToString() -like "*TSource,System.Boolean*" }
  19. $whereMethodGeneric = $whereMethod.MakeGenericMethod([Microsoft.SharePoint.Client.Field])
  20.  
  21. $fieldsQuery = $whereMethodGeneric.Invoke($Null, [System.Object[]] @($list.Fields, $expression))
  22.  
  23. #$whereMethod = [System.Linq.Queryable].GetMethod("Where", [System.Type[]] @([System.Linq.IQueryable[TSource]], [System.Linq.Expressions.Expression[System.Func[TSource,bool]]]))
  24.  
  25. $fields = $ctx.LoadQuery($fieldsQuery)
  26. $ctx.ExecuteQuery()

Note: Before using this code, you should load the necessary client object model assemblies via:

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

When validating by Fiddler, we can see that the request contains the filter we need:

image

And when checking the result, only the matching fields are returned, and all without filtering the results on the client side.

image

Conclusion: I don’t want to suggest at all that you should always use this kind of filtering when requesting data from the server. It may be unnecessary or even inefficient if you have only a few or a few dozens of entities to return from the server. However, if it is really a large number of items, it may be useful to know how to get only the entities you really need.

Note: I’ve tested my solution with SharePoint 2013 on-premise, but it should work with SharePoint 2010 and Office 365 as well. You can however apply this technique not only to the SharePoint object model as well. For example, I used it first with the Project Server 2013 Client Object Model.

May 31, 2014

How to get the SharePoint application Pool accounts from code

Filed under: PowerShell, Reflection, SharePoint — Tags: , , — Peter Holpar @ 09:01

In the past months I had a lot of trouble because of the incorrectly specified accounts on various SharePoint web and service applications, most frequently due to their missing permissions on other resources.

If you need to determine the user account configured for a web application, it is not very complicated to find out via the user interface, as described here.

You can check the account used for service application similarly using the Service Account page in the Central Administration as illustrated below.

image

Assume, you need the same information from code (C# or PowerShell) to be able to automate the verification, or simply make things to be performed faster. I’ll show you how to achieve that. It is not complicated as the UI-version at all, once you know the right path in the object model structure to dig down into the requested piece of information.

Let’s see the application pool account of a SharePoint web application first.

In the first case I assume that the code runs in the SharePoint context (like a web part):

string userName = SPContext.Current.Site.WebApplication.ApplicationPool.Username;

If you don’t have access to the context, you have to find another way to the web application:

string userName = SPWebApplication.Lookup(new Uri(http://YourSharePoint)).ApplicationPool.Username;

The PowerShell script is very simple and straightforward:

$wa = Get-SPWebApplication http://YourSharePoint
$userName = $wa.ApplicationPool.Username

Next I show, how to get the identity of a service application. If the .NET type of the service application, like the type BdcService (defined in namespace Microsoft.SharePoint.BusinessData.SharedService) for the business data connectivity service, this task is not very tricky:

BdcService bdcService = SPFarm.Local.Services.GetValue<BdcService>();
SPIisWebServiceApplication bdcApp = bdcService.Applications.FirstOrDefault() as SPIisWebServiceApplication;
// if you have multiple instances of the same type of service application, you can filter for example based on their name
// SPIisWebServiceApplication bdcApp = bdcService.Applications.FirstOrDefault(a => a.Name == "Business Data Connectivity Service") as SPIisWebServiceApplication;
string serviceAccountName = bdcApp.ApplicationPool.ProcessAccount.LookupName();

Things get a little bit more complicated, if the .NET type of the service application is defined as internal, as in the case of UserProfileService. The task can be completed using Reflection, see a similar problem and solution in my former post. Some extra lines (like checking for null values) were removed in sake of readability, but it should not affect the functionality in the standard case.

  1. // hack to get the Microsoft.Office.Server.UserProfiles assembly
  2. Assembly userProfilesAssembly = typeof(UserProfile).Assembly;
  3. // UserProfileService is an internal classes,
  4. // so you cannot get them directly from Visual Studio
  5. // like I do with the SPServiceCollection type
  6. Type userProfileServiceType = userProfilesAssembly.GetType("Microsoft.Office.Server.Administration.UserProfileService");
  7. Type spServiceCollectionType = typeof(SPServiceCollection);
  8.  
  9. // first we call
  10. // SPFarm.Local.Services.GetValue<UserProfileService>()
  11. MethodInfo mi_GetValue = spServiceCollectionType.GetMethod("GetValue",
  12.         BindingFlags.Public | BindingFlags.Instance, null, new Type[0], null
  13.         );
  14. // get the generic version of GetValue method
  15. MethodInfo mi_GetValueGeneric = mi_GetValue.MakeGenericMethod(userProfileServiceType);
  16. Object userProfileService = mi_GetValueGeneric.Invoke(SPFarm.Local.Services, null);
  17.  
  18. System.Reflection.PropertyInfo pi_Applications = userProfileServiceType.GetProperty("Applications", BindingFlags.NonPublic | BindingFlags.Instance);
  19. // userProfileApplicationCollection is of type Microsoft.Office.Server.Administration.UserProfileApplicationCollection
  20. IEnumerable<SPIisWebServiceApplication> userProfileApplicationCollection = pi_Applications.GetValue(userProfileService, null) as IEnumerable<SPIisWebServiceApplication>;
  21. SPIisWebServiceApplication userProfileApplication = userProfileApplicationCollection.FirstOrDefault();
  22. // if you have multiple instances of the same type of service application, you can filter for example based on their name
  23. //SPIisWebServiceApplication userProfileApplication = userProfileApplicationCollection.FirstOrDefault(a => a.Name == "User Profile Service Application");
  24. string serviceAccountName = userProfileApplication.ApplicationPool.ProcessAccount.LookupName();

PowerShell provides a simple solution, independently of the external visibility of the service class type. It is due to the fact that the microsoft.sharepoint.powershell assembly is defined as a friend assembly in the microsoft.sharepoint assembly, so the former one has access to the internal members of the latter one.

$ups = Get-SPServiceApplication | ? { $_.TypeName -eq "User Profile Service Application" }
$serviceAccountName  = $ups.ApplicationPool.ProcessAccount.Name

April 23, 2014

Who Has Deployed This Solution?

Filed under: PowerShell, Reflection, SP 2010, Tips & Tricks — Tags: , , , — Peter Holpar @ 22:09

Recently we had a problem in one of our server farms that was caused by a sandboxed solution that was deployed by mistake as farm solution as well. I wanted to know who has deployed the farm solution, but in the Central Administration we can see only the time of the deployment, but there is no information regarding the person who performed the action:

image

If we submit the following SQL query in the configuration database of the farm (using the name of the solution as the filter)…

  1. SELECT [Id]
  2.       ,[ClassId]
  3.       ,[ParentId]
  4.       ,[Name]
  5.       ,[Status]
  6.       ,[Version]
  7.       ,[Properties]
  8.   FROM [SharePoint_Config].[dbo].[Objects]
  9.   WHERE [Name] = 'addispname.wsp'

… we should receive two records as result that include XML objects in the Properties fields like these ones:

  1. <object type="Microsoft.SharePoint.Administration.SPPersistedFile, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
  2.     <sFld type="Int64" name="m_FileSize">4748</sFld>
  3.     <fld type="System.Collections.Hashtable, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="m_UpgradedPersistedFields" />
  4.     <fld name="m_Properties" type="null" />
  5.     <sFld type="String" name="m_LastUpdatedUser">CONTOSO\Administrator</sFld>
  6.     <sFld type="String" name="m_LastUpdatedProcess">vssphost4 (5876)</sFld>
  7.     <sFld type="String" name="m_LastUpdatedMachine">DEMO2010A</sFld>
  8.     <sFld type="DateTime" name="m_LastUpdatedTime">2013-08-22T00:01:04</sFld>
  9. </object>
  10. <object type="Microsoft.SharePoint.Administration.SPSolution, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
  11.     <sFld type="Guid" name="m_Id">31079555-a0db-4dce-8ca4-51e3e40cb6d1</sFld>
  12.     <sFld type="Boolean" name="m_WebPartPackage">False</sFld>
  13.     <sFld type="Guid" name="m_Wppid">00000000-0000-0000-0000-000000000000</sFld>
  14.     <fld type="Microsoft.SharePoint.Administration.SPServerRole, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" name="m_DeploymentServerType">WebFrontEnd</fld>
  15.     <sFld type="Boolean" name="m_HasWebAppResource">False</sFld>
  16.     <fld type="System.Collections.Hashtable, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="m_UpgradedPersistedFields" />
  17.     <fld name="m_Properties" type="null" />
  18.     <sFld type="String" name="m_LastUpdatedUser">CONTOSO\Administrator</sFld>
  19.     <sFld type="String" name="m_LastUpdatedProcess">vssphost4 (5876)</sFld>
  20.     <sFld type="String" name="m_LastUpdatedMachine">DEMO2010A</sFld>
  21.     <sFld type="DateTime" name="m_LastUpdatedTime">2013-08-22T00:01:04</sFld>
  22. </object>

As you can see, one of the objects (SPSolution) describes the solution itself, the other one (SPPeristedFile) describes the persisted file that belongs to the solution. From the properties of the persisted file we can read the information we need, see m_LastUpdatedUser.

If you don’t like the idea to query the SharePoint content database directly, there is an other way to achieve the same information, for example using PowerShell and a bit of Reflection.

First we get a reference to our solution either by ID (as long it is known):

$solution = Get-SPSolution -Identity ‘31079555-a0db-4dce-8ca4-51e3e40cb6d1’

or by Name:

$solution = Get-SPSolution | ? { $_.Name -eq ‘addispname.wsp’ }

We can get the persisted file of the solution via the SolutionFile property, then using Reflection we can read its internal LastUpdateInfo property that contains the information regarding the user that deployed the solution:

$persistedFile = $solution.SolutionFile
$persistedFileType = $persistedFile.GetType()
$bindingFlags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance
$pi_LastUpdateInfo = $persistedFileType.GetProperty(‘LastUpdateInfo’, [System.Reflection.BindingFlags]($bindingFlags))
$lastUpdateInfo = $pi_LastUpdateInfo.GetValue($persistedFile, $null)
Write-Host $lastUpdateInfo

We should have an output similar to this one:

User: CONTOSO\Administrator
Process:vssphost4 (5876)
Machine:DEMO2010A
Time:August 22, 2013 12:01:04.0000

Beyond the user, one can find other potentially interesting information as well, including the host the solution was deployed from, and the process used to deploy the solution (executable + process ID a.k.a. PID). In the sample above the solution was deployed from Visual Studio, but in other cases the value can be for example powershell (4736) that means it was deployed using PowerShell.

April 2, 2014

How to Find the Right Tool in SharePoint Central Administration Faster

Probably it’s not just because I’m a developer and not an IT-Pro, but when working with the Central Administration of SharePoint I often ask myself questions like these ones:

Where should I search the incoming e-mail configuration? In the General Application Settings? Or on the System Settings page?

As you know, the items on the Central Administration pages are stored as custom actions and custom action groups in the background. That provides a great opportunity to extend these pages via registering your own custom actions, as well makes it easy to create tools that enumerate the items on the pages, as I illustrated in this post a few years ago. I planned to create a UI extension for the Central Administration already at that time, to make it easier to find the right admin page, but until now I had not fulfilled this plan: to add a simple text input field to the start page of the Central Administration where you can type a word and an autocomplete list would display all of the matching actions.

Let’s see how to achieve that goal!

First I created a console application (SPCustomActionsExtract) that helps us to export the custom actions and groups that we will use in a JavaScript later.

In this project I defined my CustomActionGroup class that should hold the information extracted from the corresponding SPCustomActionGroupElement object. The CustomActionGroup class has the properties we need to extract, and these properties have the very same name as their counterparts in SPCustomActionGroupElement. It is important, since it makes the extraction process via Reflection very simple as we will see it later in the GetCustomActionGroup method. The class is decorated with the DataContract attribute and the properties are decorated with the DataMember attribute, since we wish to serialize the object later.

  1. [DataContract]
  2. internal class CustomActionGroup
  3. {
  4.     [DataMember]
  5.     internal string Id { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Title { get; set; }
  9.  
  10.     [DataMember]
  11.     internal string ImageUrl { get; set; }
  12.  
  13.     [DataMember]
  14.     internal List<CustomAction> CustomActions { get; set; }
  15. }

We have a similar class called CustomAction that corresponds to the SPCustomActionElement class of SharePoint. In this case, the extraction process is implemented in the GetCustomAction method (see it later).

  1. [DataContract]
  2. internal class CustomAction
  3. {
  4.     [DataMember]
  5.     internal string Title { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Description { get; set; }
  9.     
  10.     [DataMember]
  11.     internal string GroupId { get; set; }
  12.     
  13.     [DataMember]
  14.     internal string Location { get; set; }
  15.  
  16.     [DataMember]
  17.     internal string UrlAction { get; set; }
  18. }

Since we would like to use the output in JavaScript, serialization into a JSON format seems to be a good idea. I borrowed the code of the JSON serializer from this CodeProject article.

  1. internal static class JsonHelper
  2. {
  3.     public static string JsonSerializer<T>(T t)
  4.     {
  5.         DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
  6.         MemoryStream ms = new MemoryStream();
  7.         ser.WriteObject(ms, t);
  8.         string jsonString = Encoding.UTF8.GetString(ms.ToArray());
  9.         ms.Close();
  10.         return jsonString;
  11.     }  
  12. }

In the GetCustomActionGroups method we get all of the custom actions for a specific location. The SPCustomActionGroupElement instances are wrapped into CustomActionGroup objects in the GetCustomActionGroup method.

  1. private List<CustomActionGroup> GetCustomActionGroups(SPWeb web, String scope, String location)
  2. {
  3.     List<CustomActionGroup> customActionGroups = new List<CustomActionGroup>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionGroupElement> QueryForCustomActionGroups(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActionGroups = spElementProviderType.GetMethod("QueryForCustomActionGroups",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActionGroups != null)
  30.             {
  31.                 // result is List<SPCustomActionGroupElement>
  32.                 IEnumerable spCustomActionGroups = (IEnumerable)mi_QueryForCustomActionGroups.Invoke(spElementProvider,
  33.                     new Object[] { web, scope, location });
  34.  
  35.                 customActionGroups = spCustomActionGroups.Cast<Object>().AsQueryable().ToList().ConvertAll(
  36.                     spCag => GetCustomActionGroup(spCag));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActionGroups;
  42. }
  43.  
  44. private CustomActionGroup GetCustomActionGroup(object spCustomActionGroup)
  45. {
  46.     CustomActionGroup result = new CustomActionGroup();
  47.  
  48.     Type customActionGroupType = typeof(CustomActionGroup);
  49.     PropertyInfo[] cagPis = customActionGroupType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionGroupElement internal class
  54.     Type spCustomActionGroupElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionGroupElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomActionGroup.GetType() == spCustomActionGroupElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         cagPis.Where(cagPi => cagPi.PropertyType == typeof(String)).ToList().ForEach(cagPi =>
  61.         {
  62.             string propName = cagPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionGroupElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 cagPi.SetValue(result, pi.GetValue(spCustomActionGroup, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

In the GetCustomActions method we get all of the custom actions based on the location and group ID. The SPCustomActionElement instances are wrapped into CustomAction objects in the GetCustomAction method.

  1. private List<CustomAction> GetCustomActions(SPWeb web, SPList list, String scope, String location, String groupId)
  2. {
  3.     List<CustomAction> customActions = new List<CustomAction>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionElement> QueryForCustomActions(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActions = spElementProviderType.GetMethod("QueryForCustomActions",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(SPList), typeof(String), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActions != null)
  30.             {
  31.                 // result is List<SPCustomActionElement>
  32.                 IEnumerable spCustomActions = (IEnumerable)mi_QueryForCustomActions.Invoke(spElementProvider,
  33.                     new Object[] { web, list, scope, location, groupId });
  34.  
  35.                 customActions = spCustomActions.Cast<Object>().AsQueryable().ToList()
  36.                                                                   .ConvertAll(spCa => GetCustomAction(spCa));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActions;
  42. }
  43.  
  44. private CustomAction GetCustomAction(object spCustomAction)
  45. {
  46.     CustomAction result = new CustomAction();
  47.     
  48.     Type customActionType = typeof(CustomAction);
  49.     PropertyInfo[] caPis = customActionType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionElement internal class
  54.     Type spCustomActionElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomAction.GetType() == spCustomActionElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         caPis.ToList().ForEach(caPi =>
  61.         {
  62.             string propName = caPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 caPi.SetValue(result, pi.GetValue(spCustomAction, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

The possible values of the locations for the Central Administration are defined in the locations list. You find these values on MSDN. In this case we ignored the start page (location "Microsoft.SharePoint.Administration.Default"), as it contains only duplications of custom actions from other pages.

In the ExtractCustomActions method we iterate through all of these locations, get the custom action groups, and each custom actions in the groups, accumulate them into the allCustomActionGroups list, and finally save the JSON serialized result into a file.

  1. // Central Administration Custom Action Locations
  2. //http://msdn.microsoft.com/en-us/library/bb802730.aspx
  3. List<String> locations = new List<String>() {
  4.                         "Microsoft.SharePoint.Administration.Applications",
  5.                         "Microsoft.SharePoint.Administration.Backups",
  6.                         "Microsoft.SharePoint.Administration.ConfigurationWizards",
  7.                         // we don't need duplicates, so we eliminate this one
  8.                         //"Microsoft.SharePoint.Administration.Default",
  9.                         "Microsoft.SharePoint.Administration.GeneralApplicationSettings",
  10.                         "Microsoft.SharePoint.Administration.Monitoring",
  11.                         "Microsoft.SharePoint.Administration.Security",
  12.                         "Microsoft.SharePoint.Administration.SystemSettings",
  13.                         "Microsoft.SharePoint.Administration.UpgradeAndMigration"
  14.                      };
  15.  
  16.  
  17. private void ExtractCustomActions()
  18. {
  19.     List<CustomActionGroup> allCustomActionGroups = new List<CustomActionGroup>();
  20.  
  21.     // get the site collection of the Central Administration web application
  22.     SPAdministrationWebApplication centralAdmin = SPAdministrationWebApplication.Local;
  23.     using (SPSite site = centralAdmin.Sites[0])
  24.     {
  25.         using (SPWeb web = site.OpenWeb())
  26.         {
  27.             locations.ForEach(location =>
  28.                 {
  29.                     List<CustomActionGroup> customActionGroup = GetCustomActionGroups(web, null, location);
  30.                     allCustomActionGroups.AddRange(customActionGroup);
  31.                     customActionGroup.ForEach(cag =>
  32.                         {
  33.                             cag.CustomActions = GetCustomActions(web, null, null, location, cag.Id);
  34.                         });
  35.                 });
  36.         }
  37.     }
  38.  
  39.     string customActions = JsonHelper.JsonSerializer(allCustomActionGroups);
  40.     File.WriteAllText("CustomActions.json", customActions);
  41. }

We should insert the following chunk of HTML code into the start page of the Central Administration, to achieve that I inserted a Content Editor Web Part (CEWP) to the right web part zone of the page. As you can see I utilized jQuery, the LINQ for JavaScript (ver.3.0.3-Beta4) library and jQuery UI autocomplete. The single HTML element is a text field to which we can attach the autocomplete behavior.

  1. <!– jQuery –>
  2. <script type="text/javascript" src="/_layouts/CACustomActions/js/jQuery/jquery-1.8.3.min.js"></script>
  3. <!– LINQ.js –>
  4. <script src="/_layouts/CACustomActions/js/linq.min.js" type="text/javascript"></script>
  5. <!– jQuery UI autocomplete –>
  6. <script type="text/javascript" src="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/js/jquery-ui-1.10.3.custom.min.js"></script>
  7. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/css/ui-lightness/jquery-ui-1.10.3.custom.min.css">
  8. <!– Our custom .js / .css components –>
  9. <script src="/_layouts/CACustomActions/js/CustomActions.js" type="text/javascript"></script>
  10. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/css/CustomActions.css">
  11.  
  12. <input id="autocompleteCustomActions" type="text"/>

In the CustomActions.js I have a variable called CustomActions.js that contains the JSON serialized output of the custom actions and groups from our SPCustomActionsExtract tool.

On page load we register an event handler that invokes the updateAutoComplete method whenever we type a text into the text field.

  1. $(document).ready(startScript);              
  2.         
  3. function startScript() {
  4.   registerEvents();
  5. }
  6.  
  7. function registerEvents() {     
  8.        $("#autocompleteCustomActions").keyup(function(e){
  9.        updateAutoComplete();
  10.     });
  11. }

In the first part of the updateAutoComplete method we compare the titles and descriptions of the existing custom actions to the filter value we typed in, and aggregating the result into the matchingCAs array. Next, the items are ordered alphabetically

  1. var searchedCA = $("#autocompleteCustomActions").val().toLowerCase();
  2.  
  3. var matchingCAs = new Array();
  4.  
  5. Enumerable.from(customActions).forEach(function (cag) {
  6.   Enumerable.from(cag.CustomActions).forEach(function (ca) {
  7.     // find the custom action based on the title and the description
  8.     // the comparision is case insensitive
  9.     if ((ca.Title.toLowerCase().indexOf(searchedCA) > -1) || ((ca.Description != undefined) && (ca.Description.toLowerCase().indexOf(searchedCA) > -1))) {
  10.       var desc = (ca.Description != undefined) ? ca.Description  : ""
  11.       matchingCAs.push({
  12.           // HACK?: we have to create a 'label' property that inlcudes the 'Title' and the 'Description'
  13.           // otherwise the item is not shown in the autocomplete list
  14.           // I don't know if it is a feature or a bug
  15.           label: ca.Title + " – " + desc,
  16.           caption: ca.Title,
  17.           description: desc,
  18.           groupTitle: cag.Title,
  19.           urlAction: ca.UrlAction,
  20.           imageUrl: cag.ImageUrl
  21.       });
  22.     }
  23.   });
  24. });
  25.  
  26. matchingCAs = Enumerable.from(matchingCAs).orderBy("$.label").toArray();

Then we display the result in the autocomplete box. We display the icon of the custom action group and the title of the custom action in the list, but as a tool tip the description of the custom action and the title of the group will be display as well. The URL of the custom action is set as a link on the item, so if you select an item, the corresponding page will be opened in the browser.

  1. $("#autocompleteCustomActions").autocomplete({
  2.     sortResults:true,
  3.     source: matchingCAs,
  4.     open: function() {
  5.         $('#autocompleteCustomActions').autocomplete('widget').width(350);
  6.     },
  7.     focus: function(event, ui) {
  8.         $('#autocompleteCustomActions').val(ui.item.label);
  9.         return false;
  10.     }
  11. })
  12. .data("ui-autocomplete")._renderItem = function(ul, item) {
  13.     return $("<li>")
  14.     .append("<a class='ca-text' href='" + item.urlAction + "' title='" + item.description + " (" + item.groupTitle + ")'><img class='ca-img' src='"+ item.imageUrl + "' /><span class='ca-wrapper'>" + item.caption + "</span></a>")
  15.     .appendTo(ul);
  16. };

The screenshots below illustrate the functionality of the sample. As we type the text, the autocomplete list changes dynamically:

image

If you hover over a list item, additionally information is displayed as tool tip, so we can easily decide if it is really the administrative option we need. If you click on an item in the list, the corresponding page will be opened.

image

You can download the sample application (the .json generator tool, web part, .js and .css files) from here.

The deployment steps:

Copy the content of the Layouts folder from the sample into the LAYOUTS folder (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS) of your SharePoint server.

If your Central Administration contains special custom actions, you can (and should) update the value of the customActions variable at the very beginning of the CustomActions.js file (in the CACustomActions\js folder in LAYOUTS) with the content of the CustomActions.json generated by the SPCustomActionsExtract.exe tool. You should find the .json file in the bin\Debug folder of the project. The .json file I used (and that is included in the sample) was generated from a standard installation of the English version of SharePoint Server 2010 Enterprise edition, so if you have an other version (like Foundation or a German version) you should definitely regenerate the file and update the CustomActions.js file with the result.

Note: If you don’t like the idea of using a command line tool to (re)generate the structure queried from JavaScript, it is viable option to create a custom web part that performs the same actions on-demand (that means, on each page request) as this tool does, and includes the client side components (like JavaScript and css) as well. However, I found that since the registered custom actions do not change very frequently, so creating and deploying a web part seemed me a bit overkill in this case.

The web part (see WebPart\Find the Page You Need.dwp) can be imported to the start page of the the Central Administration, or if you find that easier, you can add a Content Editor Web Part (CEWP) to the page and set its source code based on the code snippet in this post above.

Older Posts »

Blog at WordPress.com.