Recently I blogged about how to implement the Where and Include functionality of the Client Object Model requests sent from PowerShell. That is great, however I don’t really like the idea to create Expression trees from scratch manually and call a lot of generic methods using Reflection to achieve this goal.
Isn’t there a simpler solution out there? Yes, there is.
In the solution illustrated below I split the functionality into two separate components. I create a static helper class that includes all of the linguistic features that are problematic to achieve form the standard PowerShell toolset.
Note: The sample below includes the Take function beyond the Where and Include methods, so only the first matching child element will be returned.
- public static class QueryHelper
- {
- public static IQueryable<Field> FilterFields(this FieldCollection fields)
- {
- return fields.Where(f => f.TypeAsString == "Guid").Take(1).Include(f => f.Id, f => f.InternalName);
- }
- }
The second component calls the helper method introduced above. This code contains only simple client object model features, all of the available via simple PowerShell method calls.
- ClientContext ctx = new ClientContext("http://sp2013");
- var list = ctx.Web.Lists.GetByTitle("Images");
- var fieldsQuery = list.Fields;
- var fields = ctx.LoadQuery(QueryHelper.FilterFields(fieldsQuery));
- ctx.ExecuteQuery();
In the next step we translate the second component to PowerShell, however we leave the C# code for the first part. From the C# code we build a temporary assembly in runtime, and call the helper method from PowerShell.
I’ve created two alternative solutions, in the first one, the code is compiled via the native Add-Type PowerShell cmdlet:
- $url = "http://sp2013"
- $referencedAssemblies = (
- "Microsoft.SharePoint.Client, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
- "Microsoft.SharePoint.Client.Runtime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
- "System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
- $sourceCode = @"
- using Microsoft.SharePoint.Client;
- using System.Collections.Generic;
- using System.Linq;
- public static class QueryHelper
- {
- public static IQueryable<Field> FilterFields(this FieldCollection fields)
- {
- return fields.Where(f => f.TypeAsString == "Guid").Take(1).Include(f => f.Id, f => f.InternalName);
- }
- }
- "@
- Add-Type -ReferencedAssemblies $referencedAssemblies -TypeDefinition $sourceCode -Language CSharp;
- 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"
- $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
- $listFields = $ctx.Web.Lists.GetByTitle("Images").Fields
- $fieldQuery = [QueryHelper]::FilterFields($listFields)
- $fields = $ctx.LoadQuery($fieldQuery)
- $ctx.ExecuteQuery()
- $fields | % { Write-Host $_.InternalName: $_.Id }
In the second one, the code is compiled directly via the CompileAssemblyFromSource method of the CSharpCodeProvider class:
- $url = "http://sp2013"
- 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"
- $sourceCode = @"
- using Microsoft.SharePoint.Client;
- using System.Collections.Generic;
- using System.Linq;
- public static class QueryHelper
- {
- public static IQueryable<Field> FilterFields(this FieldCollection fields)
- {
- return fields.Where(f => f.TypeAsString == "Guid").Take(1).Include(f => f.Id, f => f.InternalName);
- }
- }
- "@
- $parameters = New-Object System.CodeDom.Compiler.CompilerParameters
- $runtimeCompiler = New-Object Microsoft.CSharp.CSharpCodeProvider
- # supress command output by casting the command to void
- [void]$parameters.ReferencedAssemblies.Add("System.Core.dll");
- [void]$parameters.ReferencedAssemblies.Add("C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll");
- [void]$parameters.ReferencedAssemblies.Add("C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll");
- $parameters.GenerateExecutable = $False
- $parameters.GenerateInMemory = $True
- $parameters.IncludeDebugInformation = $False
- $errCount = $compRes.Errors.Count
- $compRes = $runtimeCompiler.CompileAssemblyFromSource($parameters, $sourceCode)
- Write-Host Build error count: $errCount
- If ($errCount -eq 0)
- {
- $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($url)
- $listFields = $ctx.Web.Lists.GetByTitle("Images").Fields
- $fieldQuery = [QueryHelper]::FilterFields($listFields)
- $fields = $ctx.LoadQuery($fieldQuery)
- $ctx.ExecuteQuery()
- $fields | % { $_.InternalName }
- }
The request (as checked using Fiddler) now includes the necessary filters,
and the response includes only a single entry, and the two public properties (Id and InternalName) we requested.
The output on the console:
Note: Based on my experience, the temporary assemblies are cached in the PowerShell process. That means if you alter the C# code, you should restart the PowerShell console to let the changes get reflected.
Conclusion: I prefer this approach to the former ones, because we can write the lambda expressions as we got used to in C#. There is no need to write overcomplicated Expression method calls.
