I'm working with SharePoint CSOM. This uses a query language based on expressions to specify what data to retrieve. The system I am working on makes several different queries, but they all need to include certain data for security checking. For example:
using (ClientContext context = new ClientContext(weburl))
{
var subwebs = context.Web.GetSubwebsForCurrentUser(new SubwebQuery());
context.Load(subwebs,
webs => webs.Include(
// BEGIN security-related boilerplate
web => web.RoleAssignments.Include(
ra => ra.Member,
ra => ra.RoleDefinitionBindings.Include(
rdb => rdb.BasePermissions,
rdb => rdb.RoleTypeKind
)
),
// END security-related boilerplate
web => web.Title,
web => web.ServerRelativeUrl));
context.ExecuteQuery();
}
I'm trying to avoid having to copy/paste the security-related expressions every place that they are needed. Examination of the Include method shows that it takes a list of expressions as arguments:
IQueryable<TSource> Include<TSource>(
this IQueryable<TSource> clientObjects,
params Expression<Func<TSource, object>>[] retrievals)
where TSource : ClientObject
So I created an extension method that returns the required expression:
public static Expression<Func<T, object>> GetSecurityExpression<T>(this ClientObjectCollection<T> list)
where T : SecurableObject
{
return x => x.RoleAssignments.Include(
ra => ra.Member,
ra => ra.RoleDefinitionBindings.Include(
rdb => rdb.BasePermissions,
rdb => rdb.RoleTypeKind
)
);
}
And the query is rewritten like this:
using (ClientContext context = new ClientContext(weburl))
{
var subwebs = context.Web.GetSubwebsForCurrentUser(new SubwebQuery());
context.Load(subwebs,
webs => webs.Include(
subwebs.GetSecurityExpression(),
web => web.Title,
web => web.ServerRelativeUrl));
context.ExecuteQuery();
}
This compiles fine, but at runtime it throws a Microsoft.SharePoint.Client.InvalidQueryExpressionException:
The query expression 'value(Microsoft.SharePoint.Client.WebCollection).GetSecurityExpression()' is not supported.
If I understand the exception correctly, the compiler is not calling subwebs.GetSecurityExpression(), but is creating an expression from it and passing that to Include.
The workaround I've found is to write the calling code like this:
using (ClientContext context = new ClientContext(weburl))
{
var subwebs = context.Web.GetSubwebsForCurrentUser(new SubwebQuery());
var securityExpression = subwebs.GetSecurityExpression();
context.Load(subwebs,
webs => webs.Include(
securityExpression,
web => web.Title,
web => web.ServerRelativeUrl));
context.ExecuteQuery();
}
This works, but is not as succinct as I'd like it to be. Is there any way to get the first approach to work?
The problem you face is that your method GetSecurityExpression will become part of the expression tree in the first variant, and SharePoint is not able to parse this bit. In the second variant, the method call is evaluated before the creation of the expression tree and the actual expression tree will contain the correct sub-tree (i.e. what you write inside the GetSecurityExpression method).
In principle it is possible to evaluate parts of an expression tree before executing the query. Have a look at ReLinq; it has a class PartialEvaluatingExpressionTreeVisitor which does exactly this job. However, this will come at a small performance penalty.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With