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 ListexpandPaths; /// 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 /// 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; } ///instance. /// 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 private static IQueryable InvokeSkipelements of . (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 private static IQueryable InvokeTakeelements of . (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 /// The value of the $select query option parsed into a list of lists. ///to represent the $select query option. This method assumes that $expand was already processed. And we have the tree /// of private void ApplyProjectionsToExpandTree(Listobjects for the $expand query option already built. > 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); IEnumerableordering = 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 [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 Actionthat includes query information. 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 ListexpandPaths; /// 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 /// 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; } ///instance. /// 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 private static IQueryable InvokeSkipelements of . (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 private static IQueryable InvokeTakeelements of . (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 /// The value of the $select query option parsed into a list of lists. ///to represent the $select query option. This method assumes that $expand was already processed. And we have the tree /// of private void ApplyProjectionsToExpandTree(Listobjects for the $expand query option already built. > 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); IEnumerableordering = 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 [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 Actionthat includes query information. 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
- SqlUnionizer.cs
- OdbcParameterCollection.cs
- BinaryWriter.cs
- ImageInfo.cs
- RoutedEventValueSerializer.cs
- XmlSchemaGroup.cs
- ProjectionAnalyzer.cs
- ComponentEditorForm.cs
- OlePropertyStructs.cs
- OutputScopeManager.cs
- ScriptManagerProxy.cs
- DataSourceXmlAttributeAttribute.cs
- InputMethodStateTypeInfo.cs
- CmsInterop.cs
- StoreUtilities.cs
- BitmapEffectGroup.cs
- TypeToken.cs
- XmlBufferReader.cs
- TraceFilter.cs
- MeshGeometry3D.cs
- UserControl.cs
- SizeChangedInfo.cs
- TabRenderer.cs
- ToolStripLabel.cs
- ObjectFactoryCodeDomTreeGenerator.cs
- DataGridViewRowsRemovedEventArgs.cs
- SurrogateEncoder.cs
- HttpListenerResponse.cs
- CharConverter.cs
- PseudoWebRequest.cs
- DataContractAttribute.cs
- FontWeight.cs
- precedingsibling.cs
- ToolStripPanelSelectionGlyph.cs
- Stylesheet.cs
- RuntimeHelpers.cs
- ListViewItemMouseHoverEvent.cs
- OSFeature.cs
- MailMessageEventArgs.cs
- FileVersionInfo.cs
- TableLayoutPanelDesigner.cs
- _ListenerResponseStream.cs
- PaintEvent.cs
- ListViewItemMouseHoverEvent.cs
- EntityDataSourceWrapper.cs
- LinkButton.cs
- MemoryRecordBuffer.cs
- CorrelationService.cs
- TextEditorSelection.cs
- CounterNameConverter.cs
- sitestring.cs
- SemanticResultValue.cs
- GridViewColumnCollection.cs
- Size.cs
- ForeignConstraint.cs
- Context.cs
- ObjectListCommand.cs
- DataControlLinkButton.cs
- DeviceContext2.cs
- Exceptions.cs
- Message.cs
- TextEditorTables.cs
- VarInfo.cs
- HttpHandlerActionCollection.cs
- DataStreams.cs
- DataBindingCollection.cs
- InputScopeAttribute.cs
- FieldBuilder.cs
- CodeDirectoryCompiler.cs
- StickyNoteHelper.cs
- RelationshipEndCollection.cs
- PerfCounterSection.cs
- XmlSchemaAnnotation.cs
- DesignerAdapterUtil.cs
- SHA256Managed.cs
- SafeUserTokenHandle.cs
- MetadataUtil.cs
- FilePrompt.cs
- IndexerHelper.cs
- Internal.cs
- CssTextWriter.cs
- ThousandthOfEmRealPoints.cs
- DataGridCommandEventArgs.cs
- CellRelation.cs
- CriticalFileToken.cs
- DataGridViewTextBoxEditingControl.cs
- precedingsibling.cs
- PropertyCondition.cs
- RijndaelManagedTransform.cs
- ClientSideQueueItem.cs
- AuthenticationConfig.cs
- XmlSchemaSimpleContentExtension.cs
- FileUtil.cs
- QuaternionRotation3D.cs
- ButtonBaseAutomationPeer.cs
- HtmlTableRow.cs
- FormsAuthenticationUserCollection.cs
- DataServiceExpressionVisitor.cs
- TypeBuilderInstantiation.cs
- SystemIcmpV4Statistics.cs