Code:
/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / DataWeb / Server / System / Data / Services / RequestQueryProcessor.cs / 1625574 / RequestQueryProcessor.cs
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Provides a class capable of processing query arguments.
//
//
// @owner [....]
//---------------------------------------------------------------------
namespace System.Data.Services
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Data.Services.Internal;
using System.Data.Services.Parsing;
using System.Data.Services.Providers;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
/// Use this class to process a web data service request URI.
internal struct RequestQueryProcessor
{
#region Private fields.
/// MethodInfo for Skip.
private static readonly MethodInfo InvokeSkipMethodInfo = typeof(RequestQueryProcessor).GetMethod("InvokeSkip", BindingFlags.NonPublic | BindingFlags.Static);
/// MethodInfo for Take.
private static readonly MethodInfo InvokeTakeMethodInfo = typeof(RequestQueryProcessor).GetMethod("InvokeTake", BindingFlags.NonPublic | BindingFlags.Static);
/// The generic method for CountQueryResult'[T]()
private static readonly MethodInfo countQueryResultMethodInfo = typeof(RequestQueryProcessor).GetMethod("CountQueryResult", BindingFlags.NonPublic | BindingFlags.Static);
/// Original description over which query composition takes place.
private readonly RequestDescription description;
/// Whether the $filter query option can be applied to the request.
private readonly bool filterQueryApplicable;
/// Service with data and configuration.
private readonly IDataService service;
/// Whether the $orderby, $skip, $take and $count options can be applied to the request.
private readonly bool setQueryApplicable;
/// Whether the top level request is a candidate for paging.
private readonly bool pagingApplicable;
/// Has custom paging already been applied?
private bool appliedCustomPaging;
/// List of paths to be expanded.
private List expandPaths;
/// List of paths to be expanded, before resolving the identifiers
private List> expandPathsAsText;
/// Root projection segment of the tree specifying projections and expansions for the query.
private RootProjectionNode rootProjectionNode;
/// Parser used for parsing ordering expressions
private RequestQueryParser.ExpressionParser orderingParser;
/// Collection of ordering expressions for the current query
private OrderingInfo topLevelOrderingInfo;
/// Whether any order has been applied.
private bool orderApplied;
/// Value of $skip argument
private int? skipCount;
/// Value of $top argument
private int? topCount;
/// Query being composed.
private IQueryable query;
/// Query results to be returned.
private IEnumerable queryResults;
#endregion Private fields.
/// Initializes a new instance.
/// Service with data and configuration.
/// Description for request processed so far.
private RequestQueryProcessor(IDataService service, RequestDescription description)
{
this.service = service;
this.description = description;
this.orderApplied = false;
this.skipCount = null;
this.topCount = null;
this.query = (IQueryable)description.RequestEnumerable;
this.filterQueryApplicable =
description.TargetKind == RequestTargetKind.Resource ||
description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.ComplexObject ||
(description.CountOption == RequestQueryCountOption.ValueOnly);
this.setQueryApplicable = (description.TargetKind == RequestTargetKind.Resource && !description.IsSingleResult) ||
description.CountOption == RequestQueryCountOption.ValueOnly;
// Server Driven Paging is not considered for the following cases:
// 1. Top level result is not or resource type or it is a single valued result.
// 2. $count segment provided.
// 3. Non-GET requests do not honor SDP.
// 4. Only exception for Non-GET requests is if the request is coming from a Service
// operation that returns a set of result values of entity type.
this.pagingApplicable = (description.TargetKind == RequestTargetKind.Resource && !description.IsSingleResult) &&
(description.CountOption != RequestQueryCountOption.ValueOnly) &&
!description.IsRequestForEnumServiceOperation &&
(service.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET || description.SegmentInfos[0].TargetSource == RequestTargetSource.ServiceOperation);
this.appliedCustomPaging = false;
this.expandPaths = null;
this.expandPathsAsText = null;
this.rootProjectionNode = null;
this.orderingParser = null;
this.topLevelOrderingInfo = null;
this.queryResults = null;
}
///
/// Is the top level container for the query paged i.e. we need to use StandardPaging.
///
private bool IsStandardPaged
{
get
{
if (this.pagingApplicable && !this.IsCustomPaged)
{
ResourceSetWrapper targetContainer = this.description.LastSegmentInfo.TargetContainer;
Debug.Assert(targetContainer != null, "Must have target container for non-open properties");
return targetContainer.PageSize != 0;
}
else
{
// Target container is null for OpenProperties and we have decided not to
// to enable paging for OpenType scenarios
return false;
}
}
}
/// Do we need to use CustomPaging for this service.
private bool IsCustomPaged
{
get
{
return this.service.PagingProvider.IsCustomPagedForQuery;
}
}
/// Checks that no query arguments were sent in the request.
/// Service to check.
/// true if only V2 query parameters must be checked, otherwise all the query parameters will be checked.
///
/// Regular processing checks argument applicability, but for
/// service operations that return an IEnumerable this is part
/// of the contract on service operations, rather than a structural
/// check on the request.
///
internal static void CheckEmptyQueryArguments(IDataService service, bool checkForOnlyV2QueryParameters)
{
Debug.Assert(service != null, "service != null");
DataServiceHostWrapper host = service.OperationContext.Host;
if ((!checkForOnlyV2QueryParameters &&
(!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringFilter)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringOrderBy)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSkip)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringTop)))) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringInlineCount)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSkipToken)))
{
// 400: Bad Request
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryNoOptionsApplicable);
}
}
/// Checks that no query arguments were sent in the request.
/// Service to check.
///
/// Regular processing checks argument applicability, but for
/// service operations that return an IEnumerable this is part
/// of the contract on service operations, rather than a structural
/// check on the request.
///
internal static void CheckV2EmptyQueryArguments(IDataService service)
{
Debug.Assert(service != null, "service != null");
CheckEmptyQueryArguments(service, service.Provider.IsV1Provider);
}
///
/// Composes a property navigation with the appropriate filter lamba, as appropriate.
///
/// Member access expression to compose.
/// Lambda expression used for the filter.
/// Whether null propagation is required on the .
/// The composed expression.
internal static Expression ComposePropertyNavigation(
Expression expression,
LambdaExpression filterLambda,
bool propagateNull)
{
Debug.Assert(expression != null, "expression != null");
Debug.Assert(filterLambda != null, "filterLambda != null");
Expression fixedFilter = ParameterReplacerVisitor.Replace(
filterLambda.Body,
filterLambda.Parameters[0],
expression);
Expression test;
if (propagateNull)
{
Expression nullConstant = WebUtil.NullLiteral;
test = Expression.AndAlso(Expression.NotEqual(expression, nullConstant), fixedFilter);
}
else
{
test = fixedFilter;
}
Expression conditionTrue = expression;
Expression conditionFalse = Expression.Constant(null, conditionTrue.Type);
return Expression.Condition(test, conditionTrue, conditionFalse);
}
///
/// Processes query arguments and returns a request description for
/// the resulting query.
///
/// Service with data and configuration information.
/// Description for request processed so far.
/// A new .
internal static RequestDescription ProcessQuery(IDataService service, RequestDescription description)
{
Debug.Assert(service != null, "service != null");
// In some cases, like CUD operations, we do not want to allow any query parameters to be specified.
// But in V1, we didn't have this check hence we cannot fix this now. But we need to check only for
// V2 query parameters and stop them
if ((service.OperationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET) &&
description.SegmentInfos[0].TargetSource != RequestTargetSource.ServiceOperation)
{
RequestQueryProcessor.CheckV2EmptyQueryArguments(service);
}
// When the request doesn't produce an IQueryable result,
// we can short-circuit all further processing.
if (!(description.RequestEnumerable is IQueryable))
{
RequestQueryProcessor.CheckEmptyQueryArguments(service, false /*checkForOnlyV2QueryParameters*/);
return description;
}
else
{
return new RequestQueryProcessor(service, description).ProcessQuery();
}
}
/// Generic method to invoke a Skip method on an IQueryable source.
/// Element type of the source.
/// Source query.
/// Element count to skip.
/// A new query that skips elements of .
private static IQueryable InvokeSkip(IQueryable source, int count)
{
Debug.Assert(source != null, "source != null");
IQueryable typedSource = (IQueryable)source;
return Queryable.Skip(typedSource, count);
}
/// Generic method to invoke a Take method on an IQueryable source.
/// Element type of the source.
/// Source query.
/// Element count to take.
/// A new query that takes elements of .
private static IQueryable InvokeTake(IQueryable source, int count)
{
Debug.Assert(source != null, "source != null");
IQueryable typedSource = (IQueryable)source;
return Queryable.Take(typedSource, count);
}
/// Count the query result before $top and $skip are applied
/// Element type of the source
/// Source query
/// The count from the provider
private static long CountQueryResult(IQueryable query)
{
return Queryable.LongCount(query);
}
/// Reads an $expand or $select clause.
/// Value to read.
/// True if we're parsing $select clause False for $expand
/// A list of paths, each of which is a list of identifiers.
/// Same method is used for both $expand and $select as the syntax is almost identical.
/// $select allows * at the end of the path while $expand doesn't.
private static List> ReadExpandOrSelect(string value, bool select)
{
Debug.Assert(!String.IsNullOrEmpty(value), "!String.IsNullOrEmpty(value)");
List> result = new List>();
List currentPath = null;
ExpressionLexer lexer = new ExpressionLexer(value);
while (lexer.CurrentToken.Id != TokenId.End)
{
string identifier;
bool lastSegment = false;
if (select && lexer.CurrentToken.Id == TokenId.Star)
{
identifier = lexer.CurrentToken.Text;
lexer.NextToken();
lastSegment = true;
}
else
{
identifier = lexer.ReadDottedIdentifier();
}
if (currentPath == null)
{
currentPath = new List();
result.Add(currentPath);
}
currentPath.Add(identifier);
// Check whether we're at the end, whether we're drilling in,
// or whether we're finishing with this path.
TokenId tokenId = lexer.CurrentToken.Id;
if (tokenId != TokenId.End)
{
if (lastSegment || tokenId != TokenId.Slash)
{
lexer.ValidateToken(TokenId.Comma);
currentPath = null;
}
lexer.NextToken();
}
}
return result;
}
/// Gets the root projection node or creates one if no one exists yet.
/// The root node of the projection tree.
private RootProjectionNode GetRootProjectionNode()
{
if (this.rootProjectionNode == null)
{
// Build the root of the projection and expansion tree
this.rootProjectionNode = new RootProjectionNode(
this.description.LastSegmentInfo.TargetContainer,
this.topLevelOrderingInfo,
null,
this.skipCount,
this.topCount,
null,
this.expandPaths,
this.description.TargetResourceType);
}
return this.rootProjectionNode;
}
/// Checks and resolved all textual expand paths and removes unnecessary paths.
private void CheckExpandPaths()
{
Debug.Assert(this.expandPathsAsText != null, "this.expandPaths != null");
if (this.expandPathsAsText.Count > 0 && this.query == null)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryExpandOptionNotApplicable);
}
this.expandPaths = new List(this.expandPathsAsText.Count);
for (int i = this.expandPathsAsText.Count - 1; i >= 0; i--)
{
ResourceType resourceType = this.description.TargetResourceType;
ResourceSetWrapper resourceSet = this.description.LastSegmentInfo.TargetContainer;
List path = this.expandPathsAsText[i];
ExpandSegmentCollection segments = new ExpandSegmentCollection(path.Count);
bool ignorePath = false;
for (int j = 0; j < path.Count; j++)
{
string pathSegment = path[j];
ResourceProperty property = resourceType.TryResolvePropertyName(pathSegment);
if (property == null)
{
if (resourceType.IsOpenType)
{
// Open navigation properties are not supported on OpenTypes.
throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(pathSegment));
}
throw DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(resourceType.FullName, pathSegment));
}
if (property.TypeKind == ResourceTypeKind.EntityType)
{
Expression filter = null;
resourceSet = this.service.Provider.GetContainer(resourceSet, resourceType, property);
if (resourceSet == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidPropertyNameSpecified(property.Name, resourceType.FullName));
}
resourceType = resourceSet.ResourceType;
bool singleResult = property.Kind == ResourcePropertyKind.ResourceReference;
DataServiceConfiguration.CheckResourceRightsForRead(resourceSet, singleResult);
filter = DataServiceConfiguration.ComposeQueryInterceptors(this.service, resourceSet);
if (resourceSet.PageSize != 0 && !singleResult && !this.IsCustomPaged)
{
OrderingInfo internalOrderingInfo = new OrderingInfo(true);
ParameterExpression p = Expression.Parameter(resourceType.InstanceType, "p");
foreach (var keyProp in resourceType.KeyProperties)
{
Expression e;
if (keyProp.CanReflectOnInstanceTypeProperty)
{
e = Expression.Property(p, resourceType.GetPropertyInfo(keyProp));
}
else
{
// object LateBoundMethods.GetValue(object, ResourceProperty)
e = Expression.Call(
null, /*instance*/
DataServiceProviderMethods.GetValueMethodInfo,
p,
Expression.Constant(keyProp));
e = Expression.Convert(e, keyProp.Type);
}
internalOrderingInfo.Add(new OrderingExpression(Expression.Lambda(e, p), true));
}
segments.Add(new ExpandSegment(property.Name, filter, resourceSet.PageSize, resourceSet, property, internalOrderingInfo));
// Paged expanded results force the response version to be 2.0
this.description.RaiseResponseVersion(2, 0);
}
else
{
// Expansion of collection could result in custom paging provider giving next link, so we need to set the null continuation token.
if (!singleResult && this.IsCustomPaged)
{
this.CheckAndApplyCustomPaging(null);
}
segments.Add(new ExpandSegment(property.Name, filter, this.service.Configuration.MaxResultsPerCollection, resourceSet, property, null));
}
// We need to explicitly update the feature version here since we may not bump response version
this.description.UpdateAndCheckEpmFeatureVersion(resourceSet, this.service);
// The expanded resource type may have friendly feeds. Update version of the response accordingly
//
// Note the response DSV is payload specific and since for GET we won't know if the instances to be
// serialized will contain any properties with FF KeepInContent=false until serialization time which
// happens after the headers are written, the best we can do is to determin this at the set level.
this.description.UpdateEpmResponseVersion(this.service.OperationContext.Host.RequestAccept, resourceSet, this.service.Provider);
ignorePath = false;
}
else
{
ignorePath = true;
}
}
// Even though the path might be correct, we might need to
// ignore because it's a primitive.
if (ignorePath)
{
this.expandPathsAsText.RemoveAt(i);
}
else
{
this.expandPaths.Add(segments);
// And now build the projection and expansion tree nodes for this expand path as well
ExpandedProjectionNode currentNode = this.GetRootProjectionNode();
for (int j = 0; j < segments.Count; j++)
{
ExpandSegment segment = segments[j];
ExpandedProjectionNode childNode = (ExpandedProjectionNode)currentNode.FindNode(segment.Name);
if (childNode == null)
{
childNode = new ExpandedProjectionNode(
segment.Name,
segment.ExpandedProperty,
segment.Container,
segment.OrderingInfo,
segment.Filter,
null,
segment.Container.PageSize != 0 ? (int?)segment.Container.PageSize : null,
segment.MaxResultsExpected != Int32.MaxValue ? (int?)segment.MaxResultsExpected : null);
currentNode.AddNode(childNode);
}
currentNode = childNode;
}
this.GetRootProjectionNode().ExpansionsSpecified = true;
}
}
}
/// Checks that the query option is applicable to this request.
private void CheckFilterQueryApplicable()
{
if (!this.filterQueryApplicable)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryFilterOptionNotApplicable);
}
}
/// Checks that set query options are applicable to this request.
private void CheckSetQueryApplicable()
{
if (!this.setQueryApplicable)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySetOptionsNotApplicable);
}
}
/// Processes the $expand argument of the request.
private void ProcessExpand()
{
string expand = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand);
if (!String.IsNullOrEmpty(expand))
{
this.expandPathsAsText = ReadExpandOrSelect(expand, false);
}
else
{
this.expandPathsAsText = new List>();
}
this.CheckExpandPaths();
this.service.InternalApplyingExpansions(this.query, this.expandPaths);
}
/// Builds the tree of to represent the $select query option.
/// The value of the $select query option parsed into a list of lists.
/// This method assumes that $expand was already processed. And we have the tree
/// of objects for the $expand query option already built.
private void ApplyProjectionsToExpandTree(List> selectPathsAsText)
{
for (int i = selectPathsAsText.Count - 1; i >= 0; i--)
{
List path = selectPathsAsText[i];
ExpandedProjectionNode currentNode = this.GetRootProjectionNode();
Debug.Assert(currentNode.ResourceType == this.description.TargetResourceType, "The resource type of the root doesn't match the target type of the query.");
for (int j = 0; j < path.Count; j++)
{
Debug.Assert(currentNode != null, "Can't apply projections to non-expanded nodes.");
string pathSegment = path[j];
bool lastPathSegment = (j == path.Count - 1);
currentNode.ProjectionFound = true;
// '*' is special, it means "Project all immediate properties on this level."
if (pathSegment == "*")
{
Debug.Assert(lastPathSegment, "The * select segment must be the last one. This should have been checked already by the ReadExpandOrSelect method.");
currentNode.ProjectAllImmediateProperties = true;
break;
}
ResourceType currentResourceType = currentNode.ResourceType;
ResourceProperty property = currentResourceType.TryResolvePropertyName(pathSegment);
if (property == null)
{
if (!currentResourceType.IsOpenType)
{
throw DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(currentResourceType.FullName, pathSegment));
}
if (!lastPathSegment)
{
// Open navigation properties are not supported on OpenTypes.
throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(pathSegment));
}
}
else
{
switch (property.TypeKind)
{
case ResourceTypeKind.Primitive:
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_PrimitivePropertyUsedAsNavigationProperty(currentResourceType.FullName, pathSegment));
}
break;
case ResourceTypeKind.ComplexType:
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_ComplexPropertyAsInnerSelectSegment(currentResourceType.FullName, pathSegment));
}
break;
case ResourceTypeKind.EntityType:
break;
default:
Debug.Fail("Unexpected property type.");
break;
}
}
// If we already have such segment, reuse it. We ignore duplicates on the input.
ProjectionNode newNode = currentNode.FindNode(pathSegment);
if (newNode == null)
{
// We need expanded node to already exist, otherwise the projection is invalid
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_ProjectedPropertyWithoutMatchingExpand(currentNode.PropertyName));
}
// If it's the last segment, just create a simple projection node for it
newNode = new ProjectionNode(pathSegment, property);
currentNode.AddNode(newNode);
}
currentNode = newNode as ExpandedProjectionNode;
Debug.Assert(
currentNode == null || currentNode.ResourceType == property.ResourceType,
"If we're traversing over a nav. property it's resource type must match the resource type of the expanded segment.");
// If this is the last segment in the path and it was a navigation property,
// mark it to include the entire subtree
if (lastPathSegment && currentNode != null)
{
currentNode.ProjectionFound = true;
currentNode.MarkSubtreeAsProjected();
}
}
}
}
/// Processes the $select argument of the request.
private void ProcessSelect()
{
// Parse the $select query option into paths
string select = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect);
List> selectPathsAsText;
if (!String.IsNullOrEmpty(select))
{
// Throw if $select requests have been disabled by the user
if (!this.service.Configuration.DataServiceBehavior.AcceptProjectionRequests)
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceConfiguration_ProjectionsNotSupportedInV1Server);
}
selectPathsAsText = ReadExpandOrSelect(select, true);
}
else
{
// No projections specified on the input
if (this.rootProjectionNode != null)
{
// There are expansions, but no projections
// Mark all the expanded nodes in the tree to include all properties.
this.rootProjectionNode.MarkSubtreeAsProjected();
}
return;
}
// We only allow $select on true queries (we must have IQueryable)
if (selectPathsAsText.Count > 0 && this.query == null)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// We only allow $select on entity/entityset queries. Queries which return a primitive/complex value don't support $select.
if (this.description.TargetResourceType == null || (this.description.TargetResourceType.ResourceTypeKind != ResourceTypeKind.EntityType))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// $select can't be used on $links URIs as it doesn't make sense
if (this.description.SegmentInfos.Any(si => si.TargetKind == RequestTargetKind.Link))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// We require the request to have 2.0 version when using $select
this.description.RaiseMinimumVersionRequirement(2, 0);
// We found some projections in the query - mark it as such
this.GetRootProjectionNode().ProjectionsSpecified = true;
// Apply projections to the $expand tree
this.ApplyProjectionsToExpandTree(selectPathsAsText);
// Cleanup the tree
if (this.rootProjectionNode != null)
{
// Remove all expanded nodes which are not projected
this.rootProjectionNode.RemoveNonProjectedNodes();
// Now cleanup the projected tree. We already eliminated explicit duplicates during construction
// now we want to remove redundant properties when * was used or when the subtree was already selected.
this.rootProjectionNode.ApplyWildcardsAndSort();
}
}
///
/// Generate the queryResults for the request
///
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void GenerateQueryResult()
{
if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
// $count segment will be counted last, before Expand and SDP affects the query
MethodInfo mi = countQueryResultMethodInfo.MakeGenericMethod(this.query.ElementType);
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
// set query result to Count
this.queryResults = new long[] { (long)mi.Invoke(null, new object[] { this.query }) }
.AsEnumerable();
}
else if (this.rootProjectionNode != null)
{
IExpandProvider expandProvider = this.service.Provider.GetService(this.service);
if (expandProvider != null)
{
// If top level result is paged, then we can not perform the operation since that would require
// passing in the ordering expressions for $skiptoken generation for top level results which
// the IExpandProvider interface does not support and thus we would get wrong results
if (this.IsStandardPaged)
{
throw new DataServiceException(500, Strings.DataService_SDP_TopLevelPagedResultWithOldExpandProvider);
}
// If there's projection we can't perform the operation since that would require
// passing the projection info into the IExpandProvider, which it doesn't support
// and thus would not perform the projection.
if (this.rootProjectionNode.ProjectionsSpecified)
{
throw new DataServiceException(500, Strings.DataService_Projections_ProjectionsWithOldExpandProvider);
}
// V1 behaviour of expand in this case, although this is semantically incorrect, since we are doing
// a select after orderby and skip top, which could mess up the results, assuming here that count
// has already been processed
this.ProcessOrderBy();
this.ProcessSkipAndTop();
this.queryResults = expandProvider.ApplyExpansions(this.query, this.rootProjectionNode.ExpandPaths);
// We need to mark the ExpandPaths as "possibly modified" and make serializer use those instead.
this.rootProjectionNode.UseExpandPathsForSerialization = true;
}
else
{
IProjectionProvider projectionProvider = this.service.Provider.ProjectionProvider;
// We already have the parameter information
// * Ordering expressions through ObtainOrderingExpressions
// * Skip & Top through ObtainSkipTopCounts
if (projectionProvider == null)
{
Debug.Assert(!this.service.Provider.IsV1Provider, "All V1 providers should implement the IProjectionProvider interface.");
// We are to run a query against IDSQP. Since the IProjectionProvider interface is not public
// the provider will have to be able to handle queries generated by our BasicExpandProvider
// so we should make only minimalistic assumptions about the provider's abilities.
// In here we will assume that:
// - the provider doesn't expand on demand, that is we need to generate projections for all expansions in the query
// - the provider requires correct casting to "System.Object" when we assign a value to a property of type "System.Object"
// A side effect of these assumptions is that if the provider just propagates the calls (with small modifications) to Linq to Objects
// the query should just work (nice property, as this might be rather common).
projectionProvider = new BasicExpandProvider(this.service.Provider, false, true);
}
this.query = projectionProvider.ApplyProjections(this.query, this.rootProjectionNode);
this.queryResults = this.query;
}
}
else
{
// Special case where although there were expand expressions, they were ignored because they did not refer to entity sets
if (!String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand)))
{
this.ProjectSkipTokenForNonExpand();
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
this.queryResults = this.query;
}
Debug.Assert(this.queryResults != null, "this.queryResults != null");
}
/// Processes the $filter argument of the request.
private void ProcessFilter()
{
string filter = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringFilter);
if (!String.IsNullOrEmpty(filter))
{
this.CheckFilterQueryApplicable();
this.query = RequestQueryParser.Where(this.service, this.description.LastSegmentInfo.TargetContainer, this.description.TargetResourceType, this.query, filter);
}
}
/// Processes the $skiptoken argument of the request.
private void ProcessSkipToken()
{
// Obtain skip token from query parameters.
String skipToken = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSkipToken);
if (this.pagingApplicable)
{
if (this.IsCustomPaged)
{
this.ApplyCustomPaging(skipToken);
}
else
{
this.ApplyStandardPaging(skipToken);
}
}
else
if (!String.IsNullOrEmpty(skipToken))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_SkipTokenNotAllowed);
}
}
/// Applies standard paging to the query.
/// Skip token obtained from query parameters.
private void ApplyStandardPaging(string skipToken)
{
Debug.Assert(!this.IsCustomPaged, "Custom paging should be disabled for this function to be called.");
if (!String.IsNullOrEmpty(skipToken))
{
// Parse the skipToken to obtain each positional value
KeyInstance k = null;
WebUtil.CheckSyntaxValid(KeyInstance.TryParseNullableTokens(skipToken, out k));
// Ordering constraint in the presence of skipToken will have the following settings:
// * First there will be the provided constraints based on $orderby
// * Followed by all the key columns in the resource type
// Validate that the skipToken had as many positional values as the number of ordering constraints
if (this.topLevelOrderingInfo.OrderingExpressions.Count != k.PositionalValues.Count)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_SDP_SkipTokenNotMatchingOrdering(k.PositionalValues.Count, skipToken, this.topLevelOrderingInfo.OrderingExpressions.Count));
}
// Build the Where clause with the provided skip token
this.query = RequestUriProcessor.InvokeWhereForType(
this.query,
this.orderingParser.BuildSkipTokenFilter(this.topLevelOrderingInfo, k));
// $skiptoken is expected to be only sent by 2.0 & above clients
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
}
}
/// Applies custom paging to the query.
/// Skip token obtained from query parameters.
private void ApplyCustomPaging(string skipToken)
{
Debug.Assert(this.IsCustomPaged, "Custom paging should be enabled for this function to be called.");
if (!String.IsNullOrEmpty(skipToken))
{
// Parse the skipToken to obtain each positional value
KeyInstance k = null;
WebUtil.CheckSyntaxValid(KeyInstance.TryParseNullableTokens(skipToken, out k));
ParameterExpression p = Expression.Parameter(this.description.LastSegmentInfo.TargetResourceType.InstanceType, "it");
RequestQueryParser.ExpressionParser skipTokenParser = new RequestQueryParser.ExpressionParser(
this.service,
this.description.LastSegmentInfo.TargetContainer,
this.description.LastSegmentInfo.TargetResourceType,
p,
"");
object[] convertedValues = new object[k.PositionalValues.Count];
int i = 0;
foreach (var value in k.PositionalValues)
{
convertedValues[i++] = skipTokenParser.ParseSkipTokenLiteral((string)value);
}
this.CheckAndApplyCustomPaging(convertedValues);
// $skiptoken is expected to be only sent by 2.0 & above clients
this.description.RaiseMinimumVersionRequirement(2, 0);
}
else
{
this.CheckAndApplyCustomPaging(null);
}
}
/// Processes the $orderby argument of the request.
private void ProcessOrderBy()
{
Debug.Assert(this.topLevelOrderingInfo != null, "Must have valid ordering information in ProcessOrderBy");
if (this.topLevelOrderingInfo.OrderingExpressions.Count > 0)
{
this.query = RequestQueryParser.OrderBy(this.service, this.query, this.topLevelOrderingInfo);
this.orderApplied = true;
}
}
/// Processes the $inlinecount argument of the request.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void ProcessCount()
{
string count = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringInlineCount);
if (!String.IsNullOrEmpty(count))
{
// Throw if $inlinecount requests have been disabled by the user
if (!this.service.Configuration.DataServiceBehavior.AcceptCountRequests)
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceConfiguration_CountNotSupportedInV1Server);
}
count = count.TrimStart();
// none
if (count.Equals(XmlConstants.UriRowCountOffOption))
{
return;
}
// only get
if (this.service.OperationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_RequestVerbCannotCountError);
}
// if already counting value only, then fail
if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_InlineCountWithValueCount);
}
// Only applied to a set
this.CheckSetQueryApplicable();
MethodInfo mi = countQueryResultMethodInfo.MakeGenericMethod(this.query.ElementType);
if (count == XmlConstants.UriRowCountAllOption)
{
this.description.CountValue = (long)mi.Invoke(null, new object[] { this.query });
this.description.CountOption = RequestQueryCountOption.Inline;
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
}
else
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_InvalidCountOptionError);
}
}
}
///
/// Builds the collection of ordering expressions including implicit ordering if paging is required at top level
///
private void ObtainOrderingExpressions()
{
const String Comma = ",";
const char Space = ' ';
const String AscendingOrderIdentifier = "asc";
StringBuilder orderBy = new StringBuilder(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringOrderBy));
if (orderBy.Length > 0)
{
this.CheckSetQueryApplicable();
}
Debug.Assert(this.topLevelOrderingInfo == null, "Must only be called once per query");
ResourceType rt = this.description.TargetResourceType;
Debug.Assert(rt != null || this.setQueryApplicable == false, "Resource type must be known for Ordering to be applied.");
this.topLevelOrderingInfo = new OrderingInfo(this.IsStandardPaged);
// We need to generate ordering expression, if either the result is paged, or we have
// skip or top count request because in that case, the skip or top has to happen in
// the expand provider
if (this.IsStandardPaged || this.topCount.HasValue || this.skipCount.HasValue)
{
// Additional ordering expressions in case paging is supported
String separator = orderBy.Length > 0 ? Comma : String.Empty;
foreach (var keyProp in rt.KeyProperties)
{
orderBy.Append(separator).Append(keyProp.Name).Append(Space).Append(AscendingOrderIdentifier);
separator = Comma;
}
Debug.Assert(this.query.ElementType == rt.InstanceType, "Resource type should match query element type when in this function");
}
String orderByText = orderBy.ToString();
if (!String.IsNullOrEmpty(orderByText))
{
ParameterExpression elementParameter = Expression.Parameter(rt.InstanceType, "element");
this.orderingParser = new RequestQueryParser.ExpressionParser(
this.service,
this.description.LastSegmentInfo.TargetContainer,
rt,
elementParameter,
orderByText);
IEnumerable ordering = this.orderingParser.ParseOrdering();
foreach (OrderingExpression o in ordering)
{
this.topLevelOrderingInfo.Add(new OrderingExpression(Expression.Lambda(o.Expression, elementParameter), o.IsAscending));
}
if (this.IsStandardPaged)
{
this.description.SkipTokenExpressionCount = this.topLevelOrderingInfo.OrderingExpressions.Count;
this.description.SkipTokenProperties = NeedSkipTokenVisitor.CollectSkipTokenProperties(this.topLevelOrderingInfo, rt);
}
}
}
///
/// Processes query arguments and returns a request description for
/// the resulting query.
///
/// A modified that includes query information.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private RequestDescription ProcessQuery()
{
// Obtains the values of $skip and $top arguments
this.ObtainSkipTopCounts();
// Obtain ordering information for the current request
this.ObtainOrderingExpressions();
// NOTE: The order set by ProcessOrderBy may be reset by other operations other than skip/top,
// so filtering needs to occur first.
this.ProcessFilter();
// $inlinecount ignores SDP, Skip and Top
this.ProcessCount();
// $skiptoken is just like a filter, so it should be immediately after $filter
this.ProcessSkipToken();
if (String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand))
&& String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect)))
{
this.ProjectSkipTokenForNonExpand();
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
else if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
// We need to take $top and $skip into account when executing $count requests.
// The issue is if there is a projection, we push the $skip and $top into the projection node
// and hence we miss it when $count with $select is specified.
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
// NOTE: expand goes last, as it may be reset by other operations.
this.ProcessExpand();
this.ProcessSelect();
this.GenerateQueryResult();
#if DEBUG
// We analyze the query here to detect if we have incorrectly inserted
// calls to OTM for non-open types
if (this.service.Provider.AreAllResourceTypesNonOpen)
{
// Verify that there is no call to OTMs here.
if (!(this.queryResults is BaseServiceProvider.QueryableOverEnumerable))
{
OpenTypeMethodCallDetector.CheckForOpenTypeMethodCalls((this.queryResults as IQueryable ?? this.query).Expression);
}
}
// This is a hook for exposing the query out to debuggers and tests.
Type serviceType = this.service.Instance.GetType();
while (serviceType.BaseType != null)
{
try
{
FieldInfo delegateField = serviceType.GetField("requestQueryableConstructed", BindingFlags.NonPublic | BindingFlags.Instance);
if (delegateField != null)
{
// Batch services do not have this delegate
Action action = delegateField.GetValue(this.service.Instance) as Action;
if (action != null)
{
// If the actual results are IQueryable, then report that (since it's the actual query we're going to execute)
if (this.queryResults is IQueryable)
{
action.Invoke((IQueryable)this.queryResults);
}
else
{
action.Invoke(this.query);
}
}
break;
}
serviceType = serviceType.BaseType;
}
catch (FieldAccessException)
{
// in medium trust settings, we can't get reflection permission
// just break out of the loop then
break;
}
}
#endif
Debug.Assert(this.queryResults != null, "this.queryResults != null -- otherwise ProcessExpand didn't set it");
return new RequestDescription(this.description, this.queryResults, this.rootProjectionNode);
}
///
/// In case $expand is not provided while the results are still paged, we need to create a wrapper
/// for the object in order to project the skip tokens corresponding to the result sequence
///
private void ProjectSkipTokenForNonExpand()
{
if (this.IsStandardPaged && this.description.SkipTokenProperties == null)
{
Type queryElementType = this.query.ElementType;
ParameterExpression expandParameter = Expression.Parameter(queryElementType, "p");
StringBuilder skipTokenDescription = new StringBuilder();
Type skipTokenWrapperType = this.GetSkipTokenWrapperTypeAndDescription(skipTokenDescription);
MemberBinding[] skipTokenBindings = this.GetSkipTokenBindings(skipTokenWrapperType, skipTokenDescription.ToString(), expandParameter);
Type resultWrapperType = WebUtil.GetWrapperType(new Type[] { queryElementType, skipTokenWrapperType }, null);
MemberBinding[] resultWrapperBindings = new MemberBinding[3];
resultWrapperBindings[0] = Expression.Bind(resultWrapperType.GetProperty("ExpandedElement"), expandParameter);
resultWrapperBindings[1] = Expression.Bind(resultWrapperType.GetProperty("Description"), Expression.Constant(XmlConstants.HttpQueryStringSkipToken));
resultWrapperBindings[2] = Expression.Bind(
resultWrapperType.GetProperty("ProjectedProperty0"),
Expression.MemberInit(Expression.New(skipTokenWrapperType), skipTokenBindings));
Expression resultBody = Expression.MemberInit(Expression.New(resultWrapperType), resultWrapperBindings);
this.query = RequestUriProcessor.InvokeSelectForTypes(this.query, resultWrapperType, Expression.Lambda(resultBody, expandParameter));
// Updates the ordering expressions with the ones that take the result wrapper type as parameter
// and dereference the ExpandedElement property
this.UpdateOrderingInfoWithSkipTokenWrapper(resultWrapperType);
}
}
///
/// Obtains the wrapper type for the $skiptoken along with description of properties in the wrapper
///
/// Description for the skip token properties
/// Type of $skiptoken wrapper
private Type GetSkipTokenWrapperTypeAndDescription(StringBuilder skipTokenDescription)
{
const String Comma = ",";
Type[] skipTokenTypes = new Type[this.topLevelOrderingInfo.OrderingExpressions.Count + 1];
skipTokenTypes[0] = this.query.ElementType;
int i = 0;
String separator = String.Empty;
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
skipTokenTypes[i + 1] = ((LambdaExpression)ordering.Expression).Body.Type;
skipTokenDescription.Append(separator).Append(XmlConstants.SkipTokenPropertyPrefix + i.ToString(System.Globalization.CultureInfo.InvariantCulture));
separator = Comma;
i++;
}
return WebUtil.GetWrapperType(skipTokenTypes, Strings.BasicExpandProvider_SDP_UnsupportedOrderingExpressionBreadth);
}
///
/// Given the wrapper type and description, returns bindings for the wrapper type for skip token
///
/// Wrapper type
/// Description
/// Top level parameter type
/// Array of bindings for skip token
private MemberBinding[] GetSkipTokenBindings(Type skipTokenWrapperType, String skipTokenDescription, ParameterExpression expandParameter)
{
MemberBinding[] skipTokenBindings = new MemberBinding[this.topLevelOrderingInfo.OrderingExpressions.Count + 2];
skipTokenBindings[0] = Expression.Bind(skipTokenWrapperType.GetProperty("ExpandedElement"), expandParameter);
skipTokenBindings[1] = Expression.Bind(skipTokenWrapperType.GetProperty("Description"), Expression.Constant(skipTokenDescription.ToString()));
int i = 0;
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
LambdaExpression sourceLambda = (LambdaExpression)ordering.Expression;
Expression source = ParameterReplacerVisitor.Replace(sourceLambda.Body, sourceLambda.Parameters[0], expandParameter);
MemberInfo member = skipTokenWrapperType.GetProperty("ProjectedProperty" + i.ToString(System.Globalization.CultureInfo.InvariantCulture));
skipTokenBindings[i + 2] = Expression.Bind(member, source);
i++;
}
return skipTokenBindings;
}
///
/// Updates the topLevelOrderingInfo member with the new collection of expressions that
/// dereference the ExpandedElement property on the top level wrapper object
///
/// Type of top level wrapper object
private void UpdateOrderingInfoWithSkipTokenWrapper(Type resultWrapperType)
{
OrderingInfo newOrderingInfo = new OrderingInfo(true);
ParameterExpression wrapperParameter = Expression.Parameter(resultWrapperType, "w");
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
LambdaExpression oldExpression = (LambdaExpression)ordering.Expression;
Expression newOrdering = ParameterReplacerVisitor.Replace(
oldExpression.Body,
oldExpression.Parameters[0],
Expression.MakeMemberAccess(wrapperParameter, resultWrapperType.GetProperty("ExpandedElement")));
newOrderingInfo.Add(new OrderingExpression(Expression.Lambda(newOrdering, wrapperParameter), ordering.IsAscending));
}
this.topLevelOrderingInfo = newOrderingInfo;
}
/// Processes the $skip and/or $top argument of the request by composing query with Skip and/or Take methods.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void ProcessSkipAndTop()
{
Debug.Assert(this.query != null, "this.query != null");
if (this.skipCount.HasValue)
{
Debug.Assert(this.orderApplied, "Ordering must have already been applied.");
this.query = (IQueryable)RequestQueryProcessor
.InvokeSkipMethodInfo
.MakeGenericMethod(this.query.ElementType)
.Invoke(null, new object[] { this.query, this.skipCount.Value });
Debug.Assert(this.query != null, "this.query != null");
}
if (this.topCount.HasValue)
{
Debug.Assert(this.orderApplied, "Ordering must have already been applied.");
this.query = (IQueryable)RequestQueryProcessor
.InvokeTakeMethodInfo
.MakeGenericMethod(this.query.ElementType)
.Invoke(null, new object[] { this.query, this.topCount.Value });
Debug.Assert(this.query != null, "this.query != null");
}
}
///
/// Finds out the appropriate value for skip and top parameters for the current request
///
private void ObtainSkipTopCounts()
{
int count;
if (this.ReadSkipOrTopArgument(XmlConstants.HttpQueryStringSkip, out count))
{
this.skipCount = count;
}
int pageSize = 0;
if (this.IsStandardPaged)
{
pageSize = this.description.LastSegmentInfo.TargetContainer.PageSize;
}
if (this.ReadSkipOrTopArgument(XmlConstants.HttpQueryStringTop, out count))
{
this.topCount = count;
if (this.IsStandardPaged && pageSize < this.topCount.Value)
{
// If $top is greater than or equal to page size, we will need a $skiptoken and
// thus our response will be 2.0
this.description.RaiseResponseVersion(2, 0);
this.topCount = pageSize;
}
}
else if (this.IsStandardPaged)
{
// Paging forces response version of 2.0
this.description.RaiseResponseVersion(2, 0);
this.topCount = pageSize;
}
if (this.topCount != null || this.skipCount != null)
{
this.CheckSetQueryApplicable();
}
}
///
/// Checks whether the specified argument should be processed and what
/// its value is.
///
/// Name of the query item, $top or $skip.
/// The value for the query item.
/// true if the argument should be processed; false otherwise.
private bool ReadSkipOrTopArgument(string queryItem, out int count)
{
Debug.Assert(queryItem != null, "queryItem != null");
string itemText = this.service.OperationContext.Host.GetQueryStringItem(queryItem);
if (String.IsNullOrEmpty(itemText))
{
count = 0;
return false;
}
if (!Int32.TryParse(itemText, NumberStyles.Integer, CultureInfo.InvariantCulture, out count))
{
throw DataServiceException.CreateSyntaxError(
Strings.RequestQueryProcessor_IncorrectArgumentFormat(queryItem, itemText));
}
return true;
}
/// Checks if custom paging is already applied, if not, applies it and raises response version.
/// Values of skip tokens.
private void CheckAndApplyCustomPaging(object[] skipTokenValues)
{
if (!this.appliedCustomPaging)
{
this.service.PagingProvider.PagingProviderInterface.SetContinuationToken(
this.query,
this.description.LastSegmentInfo.TargetResourceType,
skipTokenValues);
this.description.RaiseResponseVersion(2, 0);
this.appliedCustomPaging = true;
}
}
#if DEBUG
/// Detects calls to OpenTypeMethods members and asserts if it finds any.
private class OpenTypeMethodCallDetector : ALinqExpressionVisitor
{
/// Public interface for using this class.
/// Input expression.
public static void CheckForOpenTypeMethodCalls(Expression input)
{
new OpenTypeMethodCallDetector().Visit(input);
}
/// Forgiving Visit method which allows unknown expression types to pass through.
/// Input expression.
/// Visit expression.
internal override Expression Visit(Expression exp)
{
try
{
return base.Visit(exp);
}
catch (NotSupportedException)
{
return exp;
}
}
/// Method call visitor.
/// Method call being visited.
/// Whatever is provided on input.
internal override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitMethodCall(m);
}
internal override Expression VisitBinary(BinaryExpression b)
{
if (b.Method != null && b.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitBinary(b);
}
internal override Expression VisitUnary(UnaryExpression u)
{
if (u.Method != null && u.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitUnary(u);
}
}
#endif
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Provides a class capable of processing query arguments.
//
//
// @owner [....]
//---------------------------------------------------------------------
namespace System.Data.Services
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Data.Services.Internal;
using System.Data.Services.Parsing;
using System.Data.Services.Providers;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
/// Use this class to process a web data service request URI.
internal struct RequestQueryProcessor
{
#region Private fields.
/// MethodInfo for Skip.
private static readonly MethodInfo InvokeSkipMethodInfo = typeof(RequestQueryProcessor).GetMethod("InvokeSkip", BindingFlags.NonPublic | BindingFlags.Static);
/// MethodInfo for Take.
private static readonly MethodInfo InvokeTakeMethodInfo = typeof(RequestQueryProcessor).GetMethod("InvokeTake", BindingFlags.NonPublic | BindingFlags.Static);
/// The generic method for CountQueryResult'[T]()
private static readonly MethodInfo countQueryResultMethodInfo = typeof(RequestQueryProcessor).GetMethod("CountQueryResult", BindingFlags.NonPublic | BindingFlags.Static);
/// Original description over which query composition takes place.
private readonly RequestDescription description;
/// Whether the $filter query option can be applied to the request.
private readonly bool filterQueryApplicable;
/// Service with data and configuration.
private readonly IDataService service;
/// Whether the $orderby, $skip, $take and $count options can be applied to the request.
private readonly bool setQueryApplicable;
/// Whether the top level request is a candidate for paging.
private readonly bool pagingApplicable;
/// Has custom paging already been applied?
private bool appliedCustomPaging;
/// List of paths to be expanded.
private List expandPaths;
/// List of paths to be expanded, before resolving the identifiers
private List> expandPathsAsText;
/// Root projection segment of the tree specifying projections and expansions for the query.
private RootProjectionNode rootProjectionNode;
/// Parser used for parsing ordering expressions
private RequestQueryParser.ExpressionParser orderingParser;
/// Collection of ordering expressions for the current query
private OrderingInfo topLevelOrderingInfo;
/// Whether any order has been applied.
private bool orderApplied;
/// Value of $skip argument
private int? skipCount;
/// Value of $top argument
private int? topCount;
/// Query being composed.
private IQueryable query;
/// Query results to be returned.
private IEnumerable queryResults;
#endregion Private fields.
/// Initializes a new instance.
/// Service with data and configuration.
/// Description for request processed so far.
private RequestQueryProcessor(IDataService service, RequestDescription description)
{
this.service = service;
this.description = description;
this.orderApplied = false;
this.skipCount = null;
this.topCount = null;
this.query = (IQueryable)description.RequestEnumerable;
this.filterQueryApplicable =
description.TargetKind == RequestTargetKind.Resource ||
description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.ComplexObject ||
(description.CountOption == RequestQueryCountOption.ValueOnly);
this.setQueryApplicable = (description.TargetKind == RequestTargetKind.Resource && !description.IsSingleResult) ||
description.CountOption == RequestQueryCountOption.ValueOnly;
// Server Driven Paging is not considered for the following cases:
// 1. Top level result is not or resource type or it is a single valued result.
// 2. $count segment provided.
// 3. Non-GET requests do not honor SDP.
// 4. Only exception for Non-GET requests is if the request is coming from a Service
// operation that returns a set of result values of entity type.
this.pagingApplicable = (description.TargetKind == RequestTargetKind.Resource && !description.IsSingleResult) &&
(description.CountOption != RequestQueryCountOption.ValueOnly) &&
!description.IsRequestForEnumServiceOperation &&
(service.OperationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET || description.SegmentInfos[0].TargetSource == RequestTargetSource.ServiceOperation);
this.appliedCustomPaging = false;
this.expandPaths = null;
this.expandPathsAsText = null;
this.rootProjectionNode = null;
this.orderingParser = null;
this.topLevelOrderingInfo = null;
this.queryResults = null;
}
///
/// Is the top level container for the query paged i.e. we need to use StandardPaging.
///
private bool IsStandardPaged
{
get
{
if (this.pagingApplicable && !this.IsCustomPaged)
{
ResourceSetWrapper targetContainer = this.description.LastSegmentInfo.TargetContainer;
Debug.Assert(targetContainer != null, "Must have target container for non-open properties");
return targetContainer.PageSize != 0;
}
else
{
// Target container is null for OpenProperties and we have decided not to
// to enable paging for OpenType scenarios
return false;
}
}
}
/// Do we need to use CustomPaging for this service.
private bool IsCustomPaged
{
get
{
return this.service.PagingProvider.IsCustomPagedForQuery;
}
}
/// Checks that no query arguments were sent in the request.
/// Service to check.
/// true if only V2 query parameters must be checked, otherwise all the query parameters will be checked.
///
/// Regular processing checks argument applicability, but for
/// service operations that return an IEnumerable this is part
/// of the contract on service operations, rather than a structural
/// check on the request.
///
internal static void CheckEmptyQueryArguments(IDataService service, bool checkForOnlyV2QueryParameters)
{
Debug.Assert(service != null, "service != null");
DataServiceHostWrapper host = service.OperationContext.Host;
if ((!checkForOnlyV2QueryParameters &&
(!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringFilter)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringOrderBy)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSkip)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringTop)))) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringInlineCount)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect)) ||
!String.IsNullOrEmpty(host.GetQueryStringItem(XmlConstants.HttpQueryStringSkipToken)))
{
// 400: Bad Request
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryNoOptionsApplicable);
}
}
/// Checks that no query arguments were sent in the request.
/// Service to check.
///
/// Regular processing checks argument applicability, but for
/// service operations that return an IEnumerable this is part
/// of the contract on service operations, rather than a structural
/// check on the request.
///
internal static void CheckV2EmptyQueryArguments(IDataService service)
{
Debug.Assert(service != null, "service != null");
CheckEmptyQueryArguments(service, service.Provider.IsV1Provider);
}
///
/// Composes a property navigation with the appropriate filter lamba, as appropriate.
///
/// Member access expression to compose.
/// Lambda expression used for the filter.
/// Whether null propagation is required on the .
/// The composed expression.
internal static Expression ComposePropertyNavigation(
Expression expression,
LambdaExpression filterLambda,
bool propagateNull)
{
Debug.Assert(expression != null, "expression != null");
Debug.Assert(filterLambda != null, "filterLambda != null");
Expression fixedFilter = ParameterReplacerVisitor.Replace(
filterLambda.Body,
filterLambda.Parameters[0],
expression);
Expression test;
if (propagateNull)
{
Expression nullConstant = WebUtil.NullLiteral;
test = Expression.AndAlso(Expression.NotEqual(expression, nullConstant), fixedFilter);
}
else
{
test = fixedFilter;
}
Expression conditionTrue = expression;
Expression conditionFalse = Expression.Constant(null, conditionTrue.Type);
return Expression.Condition(test, conditionTrue, conditionFalse);
}
///
/// Processes query arguments and returns a request description for
/// the resulting query.
///
/// Service with data and configuration information.
/// Description for request processed so far.
/// A new .
internal static RequestDescription ProcessQuery(IDataService service, RequestDescription description)
{
Debug.Assert(service != null, "service != null");
// In some cases, like CUD operations, we do not want to allow any query parameters to be specified.
// But in V1, we didn't have this check hence we cannot fix this now. But we need to check only for
// V2 query parameters and stop them
if ((service.OperationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET) &&
description.SegmentInfos[0].TargetSource != RequestTargetSource.ServiceOperation)
{
RequestQueryProcessor.CheckV2EmptyQueryArguments(service);
}
// When the request doesn't produce an IQueryable result,
// we can short-circuit all further processing.
if (!(description.RequestEnumerable is IQueryable))
{
RequestQueryProcessor.CheckEmptyQueryArguments(service, false /*checkForOnlyV2QueryParameters*/);
return description;
}
else
{
return new RequestQueryProcessor(service, description).ProcessQuery();
}
}
/// Generic method to invoke a Skip method on an IQueryable source.
/// Element type of the source.
/// Source query.
/// Element count to skip.
/// A new query that skips elements of .
private static IQueryable InvokeSkip(IQueryable source, int count)
{
Debug.Assert(source != null, "source != null");
IQueryable typedSource = (IQueryable)source;
return Queryable.Skip(typedSource, count);
}
/// Generic method to invoke a Take method on an IQueryable source.
/// Element type of the source.
/// Source query.
/// Element count to take.
/// A new query that takes elements of .
private static IQueryable InvokeTake(IQueryable source, int count)
{
Debug.Assert(source != null, "source != null");
IQueryable typedSource = (IQueryable)source;
return Queryable.Take(typedSource, count);
}
/// Count the query result before $top and $skip are applied
/// Element type of the source
/// Source query
/// The count from the provider
private static long CountQueryResult(IQueryable query)
{
return Queryable.LongCount(query);
}
/// Reads an $expand or $select clause.
/// Value to read.
/// True if we're parsing $select clause False for $expand
/// A list of paths, each of which is a list of identifiers.
/// Same method is used for both $expand and $select as the syntax is almost identical.
/// $select allows * at the end of the path while $expand doesn't.
private static List> ReadExpandOrSelect(string value, bool select)
{
Debug.Assert(!String.IsNullOrEmpty(value), "!String.IsNullOrEmpty(value)");
List> result = new List>();
List currentPath = null;
ExpressionLexer lexer = new ExpressionLexer(value);
while (lexer.CurrentToken.Id != TokenId.End)
{
string identifier;
bool lastSegment = false;
if (select && lexer.CurrentToken.Id == TokenId.Star)
{
identifier = lexer.CurrentToken.Text;
lexer.NextToken();
lastSegment = true;
}
else
{
identifier = lexer.ReadDottedIdentifier();
}
if (currentPath == null)
{
currentPath = new List();
result.Add(currentPath);
}
currentPath.Add(identifier);
// Check whether we're at the end, whether we're drilling in,
// or whether we're finishing with this path.
TokenId tokenId = lexer.CurrentToken.Id;
if (tokenId != TokenId.End)
{
if (lastSegment || tokenId != TokenId.Slash)
{
lexer.ValidateToken(TokenId.Comma);
currentPath = null;
}
lexer.NextToken();
}
}
return result;
}
/// Gets the root projection node or creates one if no one exists yet.
/// The root node of the projection tree.
private RootProjectionNode GetRootProjectionNode()
{
if (this.rootProjectionNode == null)
{
// Build the root of the projection and expansion tree
this.rootProjectionNode = new RootProjectionNode(
this.description.LastSegmentInfo.TargetContainer,
this.topLevelOrderingInfo,
null,
this.skipCount,
this.topCount,
null,
this.expandPaths,
this.description.TargetResourceType);
}
return this.rootProjectionNode;
}
/// Checks and resolved all textual expand paths and removes unnecessary paths.
private void CheckExpandPaths()
{
Debug.Assert(this.expandPathsAsText != null, "this.expandPaths != null");
if (this.expandPathsAsText.Count > 0 && this.query == null)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryExpandOptionNotApplicable);
}
this.expandPaths = new List(this.expandPathsAsText.Count);
for (int i = this.expandPathsAsText.Count - 1; i >= 0; i--)
{
ResourceType resourceType = this.description.TargetResourceType;
ResourceSetWrapper resourceSet = this.description.LastSegmentInfo.TargetContainer;
List path = this.expandPathsAsText[i];
ExpandSegmentCollection segments = new ExpandSegmentCollection(path.Count);
bool ignorePath = false;
for (int j = 0; j < path.Count; j++)
{
string pathSegment = path[j];
ResourceProperty property = resourceType.TryResolvePropertyName(pathSegment);
if (property == null)
{
if (resourceType.IsOpenType)
{
// Open navigation properties are not supported on OpenTypes.
throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(pathSegment));
}
throw DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(resourceType.FullName, pathSegment));
}
if (property.TypeKind == ResourceTypeKind.EntityType)
{
Expression filter = null;
resourceSet = this.service.Provider.GetContainer(resourceSet, resourceType, property);
if (resourceSet == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidPropertyNameSpecified(property.Name, resourceType.FullName));
}
resourceType = resourceSet.ResourceType;
bool singleResult = property.Kind == ResourcePropertyKind.ResourceReference;
DataServiceConfiguration.CheckResourceRightsForRead(resourceSet, singleResult);
filter = DataServiceConfiguration.ComposeQueryInterceptors(this.service, resourceSet);
if (resourceSet.PageSize != 0 && !singleResult && !this.IsCustomPaged)
{
OrderingInfo internalOrderingInfo = new OrderingInfo(true);
ParameterExpression p = Expression.Parameter(resourceType.InstanceType, "p");
foreach (var keyProp in resourceType.KeyProperties)
{
Expression e;
if (keyProp.CanReflectOnInstanceTypeProperty)
{
e = Expression.Property(p, resourceType.GetPropertyInfo(keyProp));
}
else
{
// object LateBoundMethods.GetValue(object, ResourceProperty)
e = Expression.Call(
null, /*instance*/
DataServiceProviderMethods.GetValueMethodInfo,
p,
Expression.Constant(keyProp));
e = Expression.Convert(e, keyProp.Type);
}
internalOrderingInfo.Add(new OrderingExpression(Expression.Lambda(e, p), true));
}
segments.Add(new ExpandSegment(property.Name, filter, resourceSet.PageSize, resourceSet, property, internalOrderingInfo));
// Paged expanded results force the response version to be 2.0
this.description.RaiseResponseVersion(2, 0);
}
else
{
// Expansion of collection could result in custom paging provider giving next link, so we need to set the null continuation token.
if (!singleResult && this.IsCustomPaged)
{
this.CheckAndApplyCustomPaging(null);
}
segments.Add(new ExpandSegment(property.Name, filter, this.service.Configuration.MaxResultsPerCollection, resourceSet, property, null));
}
// We need to explicitly update the feature version here since we may not bump response version
this.description.UpdateAndCheckEpmFeatureVersion(resourceSet, this.service);
// The expanded resource type may have friendly feeds. Update version of the response accordingly
//
// Note the response DSV is payload specific and since for GET we won't know if the instances to be
// serialized will contain any properties with FF KeepInContent=false until serialization time which
// happens after the headers are written, the best we can do is to determin this at the set level.
this.description.UpdateEpmResponseVersion(this.service.OperationContext.Host.RequestAccept, resourceSet, this.service.Provider);
ignorePath = false;
}
else
{
ignorePath = true;
}
}
// Even though the path might be correct, we might need to
// ignore because it's a primitive.
if (ignorePath)
{
this.expandPathsAsText.RemoveAt(i);
}
else
{
this.expandPaths.Add(segments);
// And now build the projection and expansion tree nodes for this expand path as well
ExpandedProjectionNode currentNode = this.GetRootProjectionNode();
for (int j = 0; j < segments.Count; j++)
{
ExpandSegment segment = segments[j];
ExpandedProjectionNode childNode = (ExpandedProjectionNode)currentNode.FindNode(segment.Name);
if (childNode == null)
{
childNode = new ExpandedProjectionNode(
segment.Name,
segment.ExpandedProperty,
segment.Container,
segment.OrderingInfo,
segment.Filter,
null,
segment.Container.PageSize != 0 ? (int?)segment.Container.PageSize : null,
segment.MaxResultsExpected != Int32.MaxValue ? (int?)segment.MaxResultsExpected : null);
currentNode.AddNode(childNode);
}
currentNode = childNode;
}
this.GetRootProjectionNode().ExpansionsSpecified = true;
}
}
}
/// Checks that the query option is applicable to this request.
private void CheckFilterQueryApplicable()
{
if (!this.filterQueryApplicable)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryFilterOptionNotApplicable);
}
}
/// Checks that set query options are applicable to this request.
private void CheckSetQueryApplicable()
{
if (!this.setQueryApplicable)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySetOptionsNotApplicable);
}
}
/// Processes the $expand argument of the request.
private void ProcessExpand()
{
string expand = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand);
if (!String.IsNullOrEmpty(expand))
{
this.expandPathsAsText = ReadExpandOrSelect(expand, false);
}
else
{
this.expandPathsAsText = new List>();
}
this.CheckExpandPaths();
this.service.InternalApplyingExpansions(this.query, this.expandPaths);
}
/// Builds the tree of to represent the $select query option.
/// The value of the $select query option parsed into a list of lists.
/// This method assumes that $expand was already processed. And we have the tree
/// of objects for the $expand query option already built.
private void ApplyProjectionsToExpandTree(List> selectPathsAsText)
{
for (int i = selectPathsAsText.Count - 1; i >= 0; i--)
{
List path = selectPathsAsText[i];
ExpandedProjectionNode currentNode = this.GetRootProjectionNode();
Debug.Assert(currentNode.ResourceType == this.description.TargetResourceType, "The resource type of the root doesn't match the target type of the query.");
for (int j = 0; j < path.Count; j++)
{
Debug.Assert(currentNode != null, "Can't apply projections to non-expanded nodes.");
string pathSegment = path[j];
bool lastPathSegment = (j == path.Count - 1);
currentNode.ProjectionFound = true;
// '*' is special, it means "Project all immediate properties on this level."
if (pathSegment == "*")
{
Debug.Assert(lastPathSegment, "The * select segment must be the last one. This should have been checked already by the ReadExpandOrSelect method.");
currentNode.ProjectAllImmediateProperties = true;
break;
}
ResourceType currentResourceType = currentNode.ResourceType;
ResourceProperty property = currentResourceType.TryResolvePropertyName(pathSegment);
if (property == null)
{
if (!currentResourceType.IsOpenType)
{
throw DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(currentResourceType.FullName, pathSegment));
}
if (!lastPathSegment)
{
// Open navigation properties are not supported on OpenTypes.
throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(pathSegment));
}
}
else
{
switch (property.TypeKind)
{
case ResourceTypeKind.Primitive:
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_PrimitivePropertyUsedAsNavigationProperty(currentResourceType.FullName, pathSegment));
}
break;
case ResourceTypeKind.ComplexType:
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_ComplexPropertyAsInnerSelectSegment(currentResourceType.FullName, pathSegment));
}
break;
case ResourceTypeKind.EntityType:
break;
default:
Debug.Fail("Unexpected property type.");
break;
}
}
// If we already have such segment, reuse it. We ignore duplicates on the input.
ProjectionNode newNode = currentNode.FindNode(pathSegment);
if (newNode == null)
{
// We need expanded node to already exist, otherwise the projection is invalid
if (!lastPathSegment)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_ProjectedPropertyWithoutMatchingExpand(currentNode.PropertyName));
}
// If it's the last segment, just create a simple projection node for it
newNode = new ProjectionNode(pathSegment, property);
currentNode.AddNode(newNode);
}
currentNode = newNode as ExpandedProjectionNode;
Debug.Assert(
currentNode == null || currentNode.ResourceType == property.ResourceType,
"If we're traversing over a nav. property it's resource type must match the resource type of the expanded segment.");
// If this is the last segment in the path and it was a navigation property,
// mark it to include the entire subtree
if (lastPathSegment && currentNode != null)
{
currentNode.ProjectionFound = true;
currentNode.MarkSubtreeAsProjected();
}
}
}
}
/// Processes the $select argument of the request.
private void ProcessSelect()
{
// Parse the $select query option into paths
string select = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect);
List> selectPathsAsText;
if (!String.IsNullOrEmpty(select))
{
// Throw if $select requests have been disabled by the user
if (!this.service.Configuration.DataServiceBehavior.AcceptProjectionRequests)
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceConfiguration_ProjectionsNotSupportedInV1Server);
}
selectPathsAsText = ReadExpandOrSelect(select, true);
}
else
{
// No projections specified on the input
if (this.rootProjectionNode != null)
{
// There are expansions, but no projections
// Mark all the expanded nodes in the tree to include all properties.
this.rootProjectionNode.MarkSubtreeAsProjected();
}
return;
}
// We only allow $select on true queries (we must have IQueryable)
if (selectPathsAsText.Count > 0 && this.query == null)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// We only allow $select on entity/entityset queries. Queries which return a primitive/complex value don't support $select.
if (this.description.TargetResourceType == null || (this.description.TargetResourceType.ResourceTypeKind != ResourceTypeKind.EntityType))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// $select can't be used on $links URIs as it doesn't make sense
if (this.description.SegmentInfos.Any(si => si.TargetKind == RequestTargetKind.Link))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySelectOptionNotApplicable);
}
// We require the request to have 2.0 version when using $select
this.description.RaiseMinimumVersionRequirement(2, 0);
// We found some projections in the query - mark it as such
this.GetRootProjectionNode().ProjectionsSpecified = true;
// Apply projections to the $expand tree
this.ApplyProjectionsToExpandTree(selectPathsAsText);
// Cleanup the tree
if (this.rootProjectionNode != null)
{
// Remove all expanded nodes which are not projected
this.rootProjectionNode.RemoveNonProjectedNodes();
// Now cleanup the projected tree. We already eliminated explicit duplicates during construction
// now we want to remove redundant properties when * was used or when the subtree was already selected.
this.rootProjectionNode.ApplyWildcardsAndSort();
}
}
///
/// Generate the queryResults for the request
///
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void GenerateQueryResult()
{
if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
// $count segment will be counted last, before Expand and SDP affects the query
MethodInfo mi = countQueryResultMethodInfo.MakeGenericMethod(this.query.ElementType);
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
// set query result to Count
this.queryResults = new long[] { (long)mi.Invoke(null, new object[] { this.query }) }
.AsEnumerable();
}
else if (this.rootProjectionNode != null)
{
IExpandProvider expandProvider = this.service.Provider.GetService(this.service);
if (expandProvider != null)
{
// If top level result is paged, then we can not perform the operation since that would require
// passing in the ordering expressions for $skiptoken generation for top level results which
// the IExpandProvider interface does not support and thus we would get wrong results
if (this.IsStandardPaged)
{
throw new DataServiceException(500, Strings.DataService_SDP_TopLevelPagedResultWithOldExpandProvider);
}
// If there's projection we can't perform the operation since that would require
// passing the projection info into the IExpandProvider, which it doesn't support
// and thus would not perform the projection.
if (this.rootProjectionNode.ProjectionsSpecified)
{
throw new DataServiceException(500, Strings.DataService_Projections_ProjectionsWithOldExpandProvider);
}
// V1 behaviour of expand in this case, although this is semantically incorrect, since we are doing
// a select after orderby and skip top, which could mess up the results, assuming here that count
// has already been processed
this.ProcessOrderBy();
this.ProcessSkipAndTop();
this.queryResults = expandProvider.ApplyExpansions(this.query, this.rootProjectionNode.ExpandPaths);
// We need to mark the ExpandPaths as "possibly modified" and make serializer use those instead.
this.rootProjectionNode.UseExpandPathsForSerialization = true;
}
else
{
IProjectionProvider projectionProvider = this.service.Provider.ProjectionProvider;
// We already have the parameter information
// * Ordering expressions through ObtainOrderingExpressions
// * Skip & Top through ObtainSkipTopCounts
if (projectionProvider == null)
{
Debug.Assert(!this.service.Provider.IsV1Provider, "All V1 providers should implement the IProjectionProvider interface.");
// We are to run a query against IDSQP. Since the IProjectionProvider interface is not public
// the provider will have to be able to handle queries generated by our BasicExpandProvider
// so we should make only minimalistic assumptions about the provider's abilities.
// In here we will assume that:
// - the provider doesn't expand on demand, that is we need to generate projections for all expansions in the query
// - the provider requires correct casting to "System.Object" when we assign a value to a property of type "System.Object"
// A side effect of these assumptions is that if the provider just propagates the calls (with small modifications) to Linq to Objects
// the query should just work (nice property, as this might be rather common).
projectionProvider = new BasicExpandProvider(this.service.Provider, false, true);
}
this.query = projectionProvider.ApplyProjections(this.query, this.rootProjectionNode);
this.queryResults = this.query;
}
}
else
{
// Special case where although there were expand expressions, they were ignored because they did not refer to entity sets
if (!String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand)))
{
this.ProjectSkipTokenForNonExpand();
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
this.queryResults = this.query;
}
Debug.Assert(this.queryResults != null, "this.queryResults != null");
}
/// Processes the $filter argument of the request.
private void ProcessFilter()
{
string filter = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringFilter);
if (!String.IsNullOrEmpty(filter))
{
this.CheckFilterQueryApplicable();
this.query = RequestQueryParser.Where(this.service, this.description.LastSegmentInfo.TargetContainer, this.description.TargetResourceType, this.query, filter);
}
}
/// Processes the $skiptoken argument of the request.
private void ProcessSkipToken()
{
// Obtain skip token from query parameters.
String skipToken = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSkipToken);
if (this.pagingApplicable)
{
if (this.IsCustomPaged)
{
this.ApplyCustomPaging(skipToken);
}
else
{
this.ApplyStandardPaging(skipToken);
}
}
else
if (!String.IsNullOrEmpty(skipToken))
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_SkipTokenNotAllowed);
}
}
/// Applies standard paging to the query.
/// Skip token obtained from query parameters.
private void ApplyStandardPaging(string skipToken)
{
Debug.Assert(!this.IsCustomPaged, "Custom paging should be disabled for this function to be called.");
if (!String.IsNullOrEmpty(skipToken))
{
// Parse the skipToken to obtain each positional value
KeyInstance k = null;
WebUtil.CheckSyntaxValid(KeyInstance.TryParseNullableTokens(skipToken, out k));
// Ordering constraint in the presence of skipToken will have the following settings:
// * First there will be the provided constraints based on $orderby
// * Followed by all the key columns in the resource type
// Validate that the skipToken had as many positional values as the number of ordering constraints
if (this.topLevelOrderingInfo.OrderingExpressions.Count != k.PositionalValues.Count)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_SDP_SkipTokenNotMatchingOrdering(k.PositionalValues.Count, skipToken, this.topLevelOrderingInfo.OrderingExpressions.Count));
}
// Build the Where clause with the provided skip token
this.query = RequestUriProcessor.InvokeWhereForType(
this.query,
this.orderingParser.BuildSkipTokenFilter(this.topLevelOrderingInfo, k));
// $skiptoken is expected to be only sent by 2.0 & above clients
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
}
}
/// Applies custom paging to the query.
/// Skip token obtained from query parameters.
private void ApplyCustomPaging(string skipToken)
{
Debug.Assert(this.IsCustomPaged, "Custom paging should be enabled for this function to be called.");
if (!String.IsNullOrEmpty(skipToken))
{
// Parse the skipToken to obtain each positional value
KeyInstance k = null;
WebUtil.CheckSyntaxValid(KeyInstance.TryParseNullableTokens(skipToken, out k));
ParameterExpression p = Expression.Parameter(this.description.LastSegmentInfo.TargetResourceType.InstanceType, "it");
RequestQueryParser.ExpressionParser skipTokenParser = new RequestQueryParser.ExpressionParser(
this.service,
this.description.LastSegmentInfo.TargetContainer,
this.description.LastSegmentInfo.TargetResourceType,
p,
"");
object[] convertedValues = new object[k.PositionalValues.Count];
int i = 0;
foreach (var value in k.PositionalValues)
{
convertedValues[i++] = skipTokenParser.ParseSkipTokenLiteral((string)value);
}
this.CheckAndApplyCustomPaging(convertedValues);
// $skiptoken is expected to be only sent by 2.0 & above clients
this.description.RaiseMinimumVersionRequirement(2, 0);
}
else
{
this.CheckAndApplyCustomPaging(null);
}
}
/// Processes the $orderby argument of the request.
private void ProcessOrderBy()
{
Debug.Assert(this.topLevelOrderingInfo != null, "Must have valid ordering information in ProcessOrderBy");
if (this.topLevelOrderingInfo.OrderingExpressions.Count > 0)
{
this.query = RequestQueryParser.OrderBy(this.service, this.query, this.topLevelOrderingInfo);
this.orderApplied = true;
}
}
/// Processes the $inlinecount argument of the request.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void ProcessCount()
{
string count = this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringInlineCount);
if (!String.IsNullOrEmpty(count))
{
// Throw if $inlinecount requests have been disabled by the user
if (!this.service.Configuration.DataServiceBehavior.AcceptCountRequests)
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceConfiguration_CountNotSupportedInV1Server);
}
count = count.TrimStart();
// none
if (count.Equals(XmlConstants.UriRowCountOffOption))
{
return;
}
// only get
if (this.service.OperationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_RequestVerbCannotCountError);
}
// if already counting value only, then fail
if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_InlineCountWithValueCount);
}
// Only applied to a set
this.CheckSetQueryApplicable();
MethodInfo mi = countQueryResultMethodInfo.MakeGenericMethod(this.query.ElementType);
if (count == XmlConstants.UriRowCountAllOption)
{
this.description.CountValue = (long)mi.Invoke(null, new object[] { this.query });
this.description.CountOption = RequestQueryCountOption.Inline;
this.description.RaiseMinimumVersionRequirement(2, 0);
this.description.RaiseResponseVersion(2, 0);
}
else
{
throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_InvalidCountOptionError);
}
}
}
///
/// Builds the collection of ordering expressions including implicit ordering if paging is required at top level
///
private void ObtainOrderingExpressions()
{
const String Comma = ",";
const char Space = ' ';
const String AscendingOrderIdentifier = "asc";
StringBuilder orderBy = new StringBuilder(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringOrderBy));
if (orderBy.Length > 0)
{
this.CheckSetQueryApplicable();
}
Debug.Assert(this.topLevelOrderingInfo == null, "Must only be called once per query");
ResourceType rt = this.description.TargetResourceType;
Debug.Assert(rt != null || this.setQueryApplicable == false, "Resource type must be known for Ordering to be applied.");
this.topLevelOrderingInfo = new OrderingInfo(this.IsStandardPaged);
// We need to generate ordering expression, if either the result is paged, or we have
// skip or top count request because in that case, the skip or top has to happen in
// the expand provider
if (this.IsStandardPaged || this.topCount.HasValue || this.skipCount.HasValue)
{
// Additional ordering expressions in case paging is supported
String separator = orderBy.Length > 0 ? Comma : String.Empty;
foreach (var keyProp in rt.KeyProperties)
{
orderBy.Append(separator).Append(keyProp.Name).Append(Space).Append(AscendingOrderIdentifier);
separator = Comma;
}
Debug.Assert(this.query.ElementType == rt.InstanceType, "Resource type should match query element type when in this function");
}
String orderByText = orderBy.ToString();
if (!String.IsNullOrEmpty(orderByText))
{
ParameterExpression elementParameter = Expression.Parameter(rt.InstanceType, "element");
this.orderingParser = new RequestQueryParser.ExpressionParser(
this.service,
this.description.LastSegmentInfo.TargetContainer,
rt,
elementParameter,
orderByText);
IEnumerable ordering = this.orderingParser.ParseOrdering();
foreach (OrderingExpression o in ordering)
{
this.topLevelOrderingInfo.Add(new OrderingExpression(Expression.Lambda(o.Expression, elementParameter), o.IsAscending));
}
if (this.IsStandardPaged)
{
this.description.SkipTokenExpressionCount = this.topLevelOrderingInfo.OrderingExpressions.Count;
this.description.SkipTokenProperties = NeedSkipTokenVisitor.CollectSkipTokenProperties(this.topLevelOrderingInfo, rt);
}
}
}
///
/// Processes query arguments and returns a request description for
/// the resulting query.
///
/// A modified that includes query information.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private RequestDescription ProcessQuery()
{
// Obtains the values of $skip and $top arguments
this.ObtainSkipTopCounts();
// Obtain ordering information for the current request
this.ObtainOrderingExpressions();
// NOTE: The order set by ProcessOrderBy may be reset by other operations other than skip/top,
// so filtering needs to occur first.
this.ProcessFilter();
// $inlinecount ignores SDP, Skip and Top
this.ProcessCount();
// $skiptoken is just like a filter, so it should be immediately after $filter
this.ProcessSkipToken();
if (String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringExpand))
&& String.IsNullOrEmpty(this.service.OperationContext.Host.GetQueryStringItem(XmlConstants.HttpQueryStringSelect)))
{
this.ProjectSkipTokenForNonExpand();
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
else if (this.description.CountOption == RequestQueryCountOption.ValueOnly)
{
// We need to take $top and $skip into account when executing $count requests.
// The issue is if there is a projection, we push the $skip and $top into the projection node
// and hence we miss it when $count with $select is specified.
this.ProcessOrderBy();
this.ProcessSkipAndTop();
}
// NOTE: expand goes last, as it may be reset by other operations.
this.ProcessExpand();
this.ProcessSelect();
this.GenerateQueryResult();
#if DEBUG
// We analyze the query here to detect if we have incorrectly inserted
// calls to OTM for non-open types
if (this.service.Provider.AreAllResourceTypesNonOpen)
{
// Verify that there is no call to OTMs here.
if (!(this.queryResults is BaseServiceProvider.QueryableOverEnumerable))
{
OpenTypeMethodCallDetector.CheckForOpenTypeMethodCalls((this.queryResults as IQueryable ?? this.query).Expression);
}
}
// This is a hook for exposing the query out to debuggers and tests.
Type serviceType = this.service.Instance.GetType();
while (serviceType.BaseType != null)
{
try
{
FieldInfo delegateField = serviceType.GetField("requestQueryableConstructed", BindingFlags.NonPublic | BindingFlags.Instance);
if (delegateField != null)
{
// Batch services do not have this delegate
Action action = delegateField.GetValue(this.service.Instance) as Action;
if (action != null)
{
// If the actual results are IQueryable, then report that (since it's the actual query we're going to execute)
if (this.queryResults is IQueryable)
{
action.Invoke((IQueryable)this.queryResults);
}
else
{
action.Invoke(this.query);
}
}
break;
}
serviceType = serviceType.BaseType;
}
catch (FieldAccessException)
{
// in medium trust settings, we can't get reflection permission
// just break out of the loop then
break;
}
}
#endif
Debug.Assert(this.queryResults != null, "this.queryResults != null -- otherwise ProcessExpand didn't set it");
return new RequestDescription(this.description, this.queryResults, this.rootProjectionNode);
}
///
/// In case $expand is not provided while the results are still paged, we need to create a wrapper
/// for the object in order to project the skip tokens corresponding to the result sequence
///
private void ProjectSkipTokenForNonExpand()
{
if (this.IsStandardPaged && this.description.SkipTokenProperties == null)
{
Type queryElementType = this.query.ElementType;
ParameterExpression expandParameter = Expression.Parameter(queryElementType, "p");
StringBuilder skipTokenDescription = new StringBuilder();
Type skipTokenWrapperType = this.GetSkipTokenWrapperTypeAndDescription(skipTokenDescription);
MemberBinding[] skipTokenBindings = this.GetSkipTokenBindings(skipTokenWrapperType, skipTokenDescription.ToString(), expandParameter);
Type resultWrapperType = WebUtil.GetWrapperType(new Type[] { queryElementType, skipTokenWrapperType }, null);
MemberBinding[] resultWrapperBindings = new MemberBinding[3];
resultWrapperBindings[0] = Expression.Bind(resultWrapperType.GetProperty("ExpandedElement"), expandParameter);
resultWrapperBindings[1] = Expression.Bind(resultWrapperType.GetProperty("Description"), Expression.Constant(XmlConstants.HttpQueryStringSkipToken));
resultWrapperBindings[2] = Expression.Bind(
resultWrapperType.GetProperty("ProjectedProperty0"),
Expression.MemberInit(Expression.New(skipTokenWrapperType), skipTokenBindings));
Expression resultBody = Expression.MemberInit(Expression.New(resultWrapperType), resultWrapperBindings);
this.query = RequestUriProcessor.InvokeSelectForTypes(this.query, resultWrapperType, Expression.Lambda(resultBody, expandParameter));
// Updates the ordering expressions with the ones that take the result wrapper type as parameter
// and dereference the ExpandedElement property
this.UpdateOrderingInfoWithSkipTokenWrapper(resultWrapperType);
}
}
///
/// Obtains the wrapper type for the $skiptoken along with description of properties in the wrapper
///
/// Description for the skip token properties
/// Type of $skiptoken wrapper
private Type GetSkipTokenWrapperTypeAndDescription(StringBuilder skipTokenDescription)
{
const String Comma = ",";
Type[] skipTokenTypes = new Type[this.topLevelOrderingInfo.OrderingExpressions.Count + 1];
skipTokenTypes[0] = this.query.ElementType;
int i = 0;
String separator = String.Empty;
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
skipTokenTypes[i + 1] = ((LambdaExpression)ordering.Expression).Body.Type;
skipTokenDescription.Append(separator).Append(XmlConstants.SkipTokenPropertyPrefix + i.ToString(System.Globalization.CultureInfo.InvariantCulture));
separator = Comma;
i++;
}
return WebUtil.GetWrapperType(skipTokenTypes, Strings.BasicExpandProvider_SDP_UnsupportedOrderingExpressionBreadth);
}
///
/// Given the wrapper type and description, returns bindings for the wrapper type for skip token
///
/// Wrapper type
/// Description
/// Top level parameter type
/// Array of bindings for skip token
private MemberBinding[] GetSkipTokenBindings(Type skipTokenWrapperType, String skipTokenDescription, ParameterExpression expandParameter)
{
MemberBinding[] skipTokenBindings = new MemberBinding[this.topLevelOrderingInfo.OrderingExpressions.Count + 2];
skipTokenBindings[0] = Expression.Bind(skipTokenWrapperType.GetProperty("ExpandedElement"), expandParameter);
skipTokenBindings[1] = Expression.Bind(skipTokenWrapperType.GetProperty("Description"), Expression.Constant(skipTokenDescription.ToString()));
int i = 0;
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
LambdaExpression sourceLambda = (LambdaExpression)ordering.Expression;
Expression source = ParameterReplacerVisitor.Replace(sourceLambda.Body, sourceLambda.Parameters[0], expandParameter);
MemberInfo member = skipTokenWrapperType.GetProperty("ProjectedProperty" + i.ToString(System.Globalization.CultureInfo.InvariantCulture));
skipTokenBindings[i + 2] = Expression.Bind(member, source);
i++;
}
return skipTokenBindings;
}
///
/// Updates the topLevelOrderingInfo member with the new collection of expressions that
/// dereference the ExpandedElement property on the top level wrapper object
///
/// Type of top level wrapper object
private void UpdateOrderingInfoWithSkipTokenWrapper(Type resultWrapperType)
{
OrderingInfo newOrderingInfo = new OrderingInfo(true);
ParameterExpression wrapperParameter = Expression.Parameter(resultWrapperType, "w");
foreach (var ordering in this.topLevelOrderingInfo.OrderingExpressions)
{
LambdaExpression oldExpression = (LambdaExpression)ordering.Expression;
Expression newOrdering = ParameterReplacerVisitor.Replace(
oldExpression.Body,
oldExpression.Parameters[0],
Expression.MakeMemberAccess(wrapperParameter, resultWrapperType.GetProperty("ExpandedElement")));
newOrderingInfo.Add(new OrderingExpression(Expression.Lambda(newOrdering, wrapperParameter), ordering.IsAscending));
}
this.topLevelOrderingInfo = newOrderingInfo;
}
/// Processes the $skip and/or $top argument of the request by composing query with Skip and/or Take methods.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining | System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)]
private void ProcessSkipAndTop()
{
Debug.Assert(this.query != null, "this.query != null");
if (this.skipCount.HasValue)
{
Debug.Assert(this.orderApplied, "Ordering must have already been applied.");
this.query = (IQueryable)RequestQueryProcessor
.InvokeSkipMethodInfo
.MakeGenericMethod(this.query.ElementType)
.Invoke(null, new object[] { this.query, this.skipCount.Value });
Debug.Assert(this.query != null, "this.query != null");
}
if (this.topCount.HasValue)
{
Debug.Assert(this.orderApplied, "Ordering must have already been applied.");
this.query = (IQueryable)RequestQueryProcessor
.InvokeTakeMethodInfo
.MakeGenericMethod(this.query.ElementType)
.Invoke(null, new object[] { this.query, this.topCount.Value });
Debug.Assert(this.query != null, "this.query != null");
}
}
///
/// Finds out the appropriate value for skip and top parameters for the current request
///
private void ObtainSkipTopCounts()
{
int count;
if (this.ReadSkipOrTopArgument(XmlConstants.HttpQueryStringSkip, out count))
{
this.skipCount = count;
}
int pageSize = 0;
if (this.IsStandardPaged)
{
pageSize = this.description.LastSegmentInfo.TargetContainer.PageSize;
}
if (this.ReadSkipOrTopArgument(XmlConstants.HttpQueryStringTop, out count))
{
this.topCount = count;
if (this.IsStandardPaged && pageSize < this.topCount.Value)
{
// If $top is greater than or equal to page size, we will need a $skiptoken and
// thus our response will be 2.0
this.description.RaiseResponseVersion(2, 0);
this.topCount = pageSize;
}
}
else if (this.IsStandardPaged)
{
// Paging forces response version of 2.0
this.description.RaiseResponseVersion(2, 0);
this.topCount = pageSize;
}
if (this.topCount != null || this.skipCount != null)
{
this.CheckSetQueryApplicable();
}
}
///
/// Checks whether the specified argument should be processed and what
/// its value is.
///
/// Name of the query item, $top or $skip.
/// The value for the query item.
/// true if the argument should be processed; false otherwise.
private bool ReadSkipOrTopArgument(string queryItem, out int count)
{
Debug.Assert(queryItem != null, "queryItem != null");
string itemText = this.service.OperationContext.Host.GetQueryStringItem(queryItem);
if (String.IsNullOrEmpty(itemText))
{
count = 0;
return false;
}
if (!Int32.TryParse(itemText, NumberStyles.Integer, CultureInfo.InvariantCulture, out count))
{
throw DataServiceException.CreateSyntaxError(
Strings.RequestQueryProcessor_IncorrectArgumentFormat(queryItem, itemText));
}
return true;
}
/// Checks if custom paging is already applied, if not, applies it and raises response version.
/// Values of skip tokens.
private void CheckAndApplyCustomPaging(object[] skipTokenValues)
{
if (!this.appliedCustomPaging)
{
this.service.PagingProvider.PagingProviderInterface.SetContinuationToken(
this.query,
this.description.LastSegmentInfo.TargetResourceType,
skipTokenValues);
this.description.RaiseResponseVersion(2, 0);
this.appliedCustomPaging = true;
}
}
#if DEBUG
/// Detects calls to OpenTypeMethods members and asserts if it finds any.
private class OpenTypeMethodCallDetector : ALinqExpressionVisitor
{
/// Public interface for using this class.
/// Input expression.
public static void CheckForOpenTypeMethodCalls(Expression input)
{
new OpenTypeMethodCallDetector().Visit(input);
}
/// Forgiving Visit method which allows unknown expression types to pass through.
/// Input expression.
/// Visit expression.
internal override Expression Visit(Expression exp)
{
try
{
return base.Visit(exp);
}
catch (NotSupportedException)
{
return exp;
}
}
/// Method call visitor.
/// Method call being visited.
/// Whatever is provided on input.
internal override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitMethodCall(m);
}
internal override Expression VisitBinary(BinaryExpression b)
{
if (b.Method != null && b.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitBinary(b);
}
internal override Expression VisitUnary(UnaryExpression u)
{
if (u.Method != null && u.Method.DeclaringType == typeof(OpenTypeMethods))
{
throw new InvalidOperationException("Unexpected call to OpenTypeMethods found in query when the provider did not have any OpenTypes.");
}
return base.VisitUnary(u);
}
}
#endif
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- StdValidatorsAndConverters.cs
- HtmlTernaryTree.cs
- Canvas.cs
- GeometryCollection.cs
- EventDrivenDesigner.cs
- RawStylusInputCustomDataList.cs
- TokenizerHelper.cs
- TemplateManager.cs
- SqlIdentifier.cs
- WorkflowMarkupSerializerMapping.cs
- ChameleonKey.cs
- XmlSchemaComplexContentExtension.cs
- RoutingSection.cs
- ContainerControl.cs
- TypePropertyEditor.cs
- XamlReaderHelper.cs
- TextProperties.cs
- GridToolTip.cs
- UIElementPropertyUndoUnit.cs
- TriggerActionCollection.cs
- LinkedResourceCollection.cs
- RelOps.cs
- Run.cs
- ArrangedElement.cs
- ReadOnlyDataSourceView.cs
- VarRefManager.cs
- PolygonHotSpot.cs
- ScriptingRoleServiceSection.cs
- LookupBindingPropertiesAttribute.cs
- SqlRowUpdatingEvent.cs
- SqlDataSourceQueryConverter.cs
- WebAdminConfigurationHelper.cs
- FloatUtil.cs
- RightsManagementEncryptionTransform.cs
- WebOperationContext.cs
- WebEventTraceProvider.cs
- AccessDataSource.cs
- Configuration.cs
- FixUpCollection.cs
- AsmxEndpointPickerExtension.cs
- StatementContext.cs
- FormViewInsertedEventArgs.cs
- SchemaLookupTable.cs
- DataKey.cs
- SystemUdpStatistics.cs
- TimeSpan.cs
- ProfileEventArgs.cs
- UInt16.cs
- MultiSelectRootGridEntry.cs
- DynamicRendererThreadManager.cs
- EditBehavior.cs
- InteropEnvironment.cs
- StringValidator.cs
- DesigntimeLicenseContext.cs
- UnhandledExceptionEventArgs.cs
- WindowsComboBox.cs
- SafeFileHandle.cs
- CharacterBuffer.cs
- GeneralTransform2DTo3D.cs
- BitmapImage.cs
- DataServiceException.cs
- MruCache.cs
- PropagationProtocolsTracing.cs
- TextHidden.cs
- DataRelationCollection.cs
- BoundColumn.cs
- Rotation3DAnimationBase.cs
- URLIdentityPermission.cs
- ButtonBase.cs
- CategoryNameCollection.cs
- Fonts.cs
- KeyPullup.cs
- AnnouncementSendsAsyncResult.cs
- IDictionary.cs
- BaseContextMenu.cs
- DodSequenceMerge.cs
- XsltInput.cs
- CroppedBitmap.cs
- WaitForChangedResult.cs
- MdiWindowListItemConverter.cs
- ExpandCollapsePattern.cs
- WebServiceParameterData.cs
- basecomparevalidator.cs
- Schedule.cs
- SeekStoryboard.cs
- IteratorFilter.cs
- LazyLoadBehavior.cs
- RepeatInfo.cs
- TableItemStyle.cs
- ByValueEqualityComparer.cs
- DataRecordObjectView.cs
- VarInfo.cs
- DefinitionBase.cs
- Parser.cs
- SchemaImporter.cs
- ChildDocumentBlock.cs
- FormatControl.cs
- DataReceivedEventArgs.cs
- Vector3dCollection.cs
- DialogResultConverter.cs