/// <summary>
        /// Get a single or default value from a collection.
        /// </summary>
        /// <param name="queryable">The response value as <see cref="IQueryable"/>.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <returns></returns>
        internal static object SingleOrDefault(
            IQueryable queryable,
            IWebApiActionDescriptor actionDescriptor)
        {
            var enumerator = queryable.GetEnumerator();

            try
            {
                var result = enumerator.MoveNext() ? enumerator.Current : null;

                if (enumerator.MoveNext())
                {
                    throw new InvalidOperationException(Error.Format(
                                                            SRResources.SingleResultHasMoreThanOneEntity,
                                                            actionDescriptor.ActionName,
                                                            actionDescriptor.ControllerName,
                                                            "SingleResult"));
                }

                return(result);
            }
            finally
            {
                // Ensure any active/open database objects that were created
                // iterating over the IQueryable object are properly closed.
                var disposable = enumerator as IDisposable;
                if (disposable != null)
                {
                    disposable.Dispose();
                }
            }
        }
        /// <summary>
        /// Get the ODaya query context.
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="path">The OData path.</param>
        /// <returns></returns>
        private static ODataQueryContext GetODataQueryContext(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            ODataPath path)
        {
            Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor);

            IEdmModel model = modelFunction(elementClrType);

            if (model == null)
            {
                throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull);
            }

            IEdmType elementType = null;
            IEdmModelClrTypeMappingHandler typeMappingHandler = model.GetAnnotationValue <IEdmModelClrTypeMappingHandler>(model);

            if (typeMappingHandler != null)
            {
                elementType = typeMappingHandler.MapClrInstanceToEdmType(model, responseValue);
                elementType = EdmLibHelpers.UnwrapCollectionType(elementType);
            }

            if (elementType == null)
            {
                elementType = model.GetEdmType(elementClrType);
            }

            return(new ODataQueryContext(model, elementType, elementClrType, path));
        }
        /// <summary>
        /// Get the element type.
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <returns></returns>
        internal static Type GetElementType(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor)
        {
            Contract.Assert(responseValue != null);

            IEnumerable enumerable = responseValue as IEnumerable;

            if (enumerable == null)
            {
                if (singleResultCollection == null)
                {
                    return(responseValue.GetType());
                }

                enumerable = singleResultCollection as IEnumerable;
            }

            Type elementClrType = TypeHelper.GetImplementedIEnumerableType(enumerable.GetType());

            if (elementClrType == null)
            {
                // The element type cannot be determined because the type of the content
                // is not IEnumerable<T> or IQueryable<T>.
                throw Error.InvalidOperation(
                          SRResources.FailedToRetrieveTypeToBuildEdmModel,
                          typeof(EnableQueryAttribute).Name,
                          actionDescriptor.ActionName,
                          actionDescriptor.ControllerName,
                          responseValue.GetType().FullName);
            }

            return(elementClrType);
        }
        /// <summary>
        /// Get the page size.
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="path">The OData path.</param>
        /// <param name="createErrorAction">A function used to generate error response.</param>
        private void GetModelBoundPageSize(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            ODataPath path,
            Action <HttpStatusCode, string, Exception> createErrorAction)
        {
            ODataQueryContext queryContext = null;

            try
            {
                queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, modelFunction, path);
            }
            catch (InvalidOperationException e)
            {
                createErrorAction(
                    HttpStatusCode.BadRequest,
                    Error.Format(SRResources.UriQueryStringInvalid, e.Message),
                    e);
                return;
            }

            ModelBoundQuerySettings querySettings = EdmLibHelpers.GetModelBoundQuerySettings(queryContext.TargetProperty,
                                                                                             queryContext.TargetStructuredType,
                                                                                             queryContext.Model);

            if (querySettings != null && querySettings.PageSize.HasValue)
            {
                _querySettings.ModelBoundPageSize = querySettings.PageSize;
            }
        }
        /// <inheritdoc />
        internal static SelectControllerResult SelectControllerImpl(ODataPath odataPath, IWebApiRequestMessage request,
                                                                    IDictionary <ODataPathTemplate, IWebApiActionDescriptor> attributeMappings)
        {
            Dictionary <string, object> values = new Dictionary <string, object>();

            foreach (KeyValuePair <ODataPathTemplate, IWebApiActionDescriptor> attributeMapping in attributeMappings)
            {
                ODataPathTemplate       template = attributeMapping.Key;
                IWebApiActionDescriptor action   = attributeMapping.Value;

                if (action.IsHttpMethodSupported(request.GetRequestMethodOrPreflightMethod()) && template.TryMatch(odataPath, values))
                {
                    values["action"] = action.ActionName;
                    SelectControllerResult result = new SelectControllerResult(action.ControllerName, values);

                    return(result);
                }

                // It's possible that template.TryMatch inserted values in the values dict even if
                // it did not match the current path. So let's clear the dict before trying
                // the next template
                values.Clear();
            }

            return(null);
        }
        /// <summary>
        /// Selects the action for OData requests.
        /// </summary>
        /// <param name="odataPath">The OData path.</param>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="actionMap">The action map.</param>
        /// <returns>
        ///   <c>null</c> if the request isn't handled by this convention; otherwise, the name of the selected action
        /// </returns>
        internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerContext controllerContext, IWebApiActionMap actionMap)
        {
            ODataRequestMethod method = controllerContext.Request.GetRequestMethodOrPreflightMethod();

            if (method != ODataRequestMethod.Get)
            {
                // [EnableNestedPaths] only supports GET requests
                return(null);
            }

            // unsupported path segments
            if (odataPath.PathTemplate.EndsWith("$ref"))
            {
                return(null);
            }

            ODataPathSegment firstSegment = odataPath.Segments.FirstOrDefault();

            string sourceName;

            if (firstSegment is EntitySetSegment entitySetSegment)
            {
                sourceName = entitySetSegment.EntitySet.Name;
            }
            else if (firstSegment is SingletonSegment singletonSegment)
            {
                sourceName = singletonSegment.Singleton.Name;
            }
            else
            {
                // this only supports paths starting with an entity set or singleton
                return(null);
            }

            // if we did not find a matching action amongst the conventional user-defined methods
            // then let's check if the controller has a Get method with [EnableNestedPaths] attribute
            // which should be used to catch any nested GET request
            string action = actionMap.FindMatchingAction("Get" + sourceName, "Get");

            if (action == null)
            {
                return(null);
            }

            IWebApiActionDescriptor descriptor = actionMap.GetActionDescriptor(action);

            if (descriptor == null)
            {
                return(null);
            }

            if (!descriptor.GetCustomAttributes <EnableNestedPathsAttribute>(/* inherit */ true).Any())
            {
                return(null);
            }

            return(descriptor.ActionName);
        }
        /// <summary>
        /// Execute the query.
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="request">The internal request.</param>
        /// <param name="createQueryOptionFunction">A function used to create and validate query options.</param>
        /// <returns></returns>
        private object ExecuteQuery(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            IWebApiRequestMessage request,
            Func <ODataQueryContext, IODataQueryOptions> createQueryOptionFunction)
        {
            ODataQueryContext queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, modelFunction, request.Context.Path);

            // Create and validate the query options.
            IODataQueryOptions queryOptions = createQueryOptionFunction(queryContext);

            // apply the query
            IEnumerable enumerable = responseValue as IEnumerable;

            if (enumerable == null || responseValue is string || responseValue is byte[])
            {
                // response is not a collection; we only support $select and $expand on single entities.
                ValidateSelectExpandOnly(queryOptions);

                if (singleResultCollection == null)
                {
                    // response is a single entity.
                    return(ApplyQuery(entity: responseValue, queryOptions: queryOptions));
                }
                else
                {
                    IQueryable queryable = singleResultCollection as IQueryable;
                    queryable = ApplyQuery(queryable, queryOptions);
                    return(SingleOrDefault(queryable, actionDescriptor));
                }
            }
            else
            {
                // response is a collection.
                IQueryable queryable = (enumerable as IQueryable) ?? enumerable.AsQueryable();
                queryable = ApplyQuery(queryable, queryOptions);

                if (request.IsCountRequest())
                {
                    long?count = request.Context.TotalCount;

                    if (count.HasValue)
                    {
                        // Return the count value if it is a $count request.
                        return(count.Value);
                    }
                }

                return(queryable);
            }
        }
        /// <summary>
        /// Get the ODaya query context.
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="path">The OData path.</param>
        /// <returns></returns>
        private static ODataQueryContext GetODataQueryContext(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            ODataPath path)
        {
            Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor);

            IEdmModel model = modelFunction(elementClrType);

            if (model == null)
            {
                throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull);
            }

            return(new ODataQueryContext(model, elementClrType, path));
        }
Example #9
0
        /// <summary>
        /// Determine if the
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="path">The OData path.</param>
        /// <returns></returns>
        private static bool ContainsAutoSelectExpandProperty(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            ODataPath path)
        {
            Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor);

            IEdmModel model = modelFunction(elementClrType);

            if (model == null)
            {
                throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull);
            }

            IEdmType           edmType        = model.GetTypeMappingCache().GetEdmType(elementClrType, model)?.Definition;
            IEdmStructuredType structuredType = edmType as IEdmStructuredType;

            IEdmStructuredType pathStructuredType = null;
            IEdmProperty       pathProperty       = null;

            if (path != null)
            {
                EdmLibHelpers.GetPropertyAndStructuredTypeFromPath(path.Segments, out pathProperty,
                                                                   out pathStructuredType,
                                                                   out _);
            }

            // Take the type and property from path first, it's higher priority than the value type.
            if (pathStructuredType != null && pathProperty != null)
            {
                return(model.HasAutoExpandProperty(pathStructuredType, pathProperty) || model.HasAutoSelectProperty(pathStructuredType, pathProperty));
            }
            else if (structuredType != null)
            {
                return(model.HasAutoExpandProperty(structuredType, null) || model.HasAutoSelectProperty(structuredType, null));
            }

            return(false);
        }
Example #10
0
        /// <inheritdoc />
        internal static SelectControllerResult SelectControllerImpl(ODataPath odataPath, IWebApiRequestMessage request,
                                                                    IDictionary <ODataPathTemplate, IWebApiActionDescriptor> attributeMappings)
        {
            Dictionary <string, object> values = new Dictionary <string, object>();

            foreach (KeyValuePair <ODataPathTemplate, IWebApiActionDescriptor> attributeMapping in attributeMappings)
            {
                ODataPathTemplate       template = attributeMapping.Key;
                IWebApiActionDescriptor action   = attributeMapping.Value;

                if (action.IsHttpMethodSupported(request.GetRequestMethodOrPreflightMethod()) && template.TryMatch(odataPath, values))
                {
                    values["action"] = action.ActionName;
                    SelectControllerResult result = new SelectControllerResult(action.ControllerName, values);

                    return(result);
                }
            }

            return(null);
        }
Example #11
0
        /// <summary>
        /// Determine if the
        /// </summary>
        /// <param name="responseValue">The response value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="path">The OData path.</param>
        /// <returns></returns>
        private static bool ContainsAutoSelectExpandProperty(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            Func <Type, IEdmModel> modelFunction,
            ODataPath path)
        {
            Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor);

            IEdmModel model = modelFunction(elementClrType);

            if (model == null)
            {
                throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull);
            }

            IEdmType           edmType        = model.GetTypeMappingCache().GetEdmType(elementClrType, model)?.Definition;
            IEdmEntityType     baseEntityType = edmType as IEdmEntityType;
            IEdmStructuredType structuredType = edmType as IEdmStructuredType;
            IEdmProperty       property       = null;

            if (path != null)
            {
                string name;
                EdmLibHelpers.GetPropertyAndStructuredTypeFromPath(path.Segments, out property,
                                                                   out structuredType,
                                                                   out name);
            }

            if (baseEntityType != null)
            {
                List <IEdmEntityType> entityTypes = new List <IEdmEntityType>();
                entityTypes.Add(baseEntityType);
                entityTypes.AddRange(EdmLibHelpers.GetAllDerivedEntityTypes(baseEntityType, model));
                foreach (var entityType in entityTypes)
                {
                    IEnumerable <IEdmNavigationProperty> navigationProperties = entityType == baseEntityType
                        ? entityType.NavigationProperties()
                        : entityType.DeclaredNavigationProperties();

                    if (navigationProperties != null)
                    {
                        if (navigationProperties.Any(
                                navigationProperty =>
                                EdmLibHelpers.IsAutoExpand(navigationProperty, property, entityType, model)))
                        {
                            return(true);
                        }
                    }

                    IEnumerable <IEdmStructuralProperty> properties = entityType == baseEntityType
                        ? entityType.StructuralProperties()
                        : entityType.DeclaredStructuralProperties();

                    if (properties != null)
                    {
                        foreach (var edmProperty in properties)
                        {
                            if (EdmLibHelpers.IsAutoSelect(edmProperty, property, entityType, model))
                            {
                                return(true);
                            }
                        }
                    }
                }
            }
            else if (structuredType != null)
            {
                IEnumerable <IEdmStructuralProperty> properties = structuredType.StructuralProperties();
                if (properties != null)
                {
                    foreach (var edmProperty in properties)
                    {
                        if (EdmLibHelpers.IsAutoSelect(edmProperty, property, structuredType, model))
                        {
                            return(true);
                        }
                    }
                }
            }

            return(false);
        }
Example #12
0
        /// <summary>
        /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the
        /// returning response message. It then validates the query from uri based on the validation settings on
        /// <see cref="EnableQueryAttribute"/>. It finally applies the query appropriately, and reset it back on
        /// the response message.
        /// </summary>
        /// <param name="responseValue">The response content value.</param>
        /// <param name="singleResultCollection">The content as SingleResult.Queryable.</param>
        /// <param name="actionDescriptor">The action context, i.e. action and controller name.</param>
        /// <param name="request">The internal request.</param>
        /// <param name="modelFunction">A function to get the model.</param>
        /// <param name="createQueryOptionFunction">A function used to create and validate query options.</param>
        /// <param name="createResponseAction">An action used to create a response.</param>
        /// <param name="createErrorAction">A function used to generate error response.</param>
        private object OnActionExecuted(
            object responseValue,
            IQueryable singleResultCollection,
            IWebApiActionDescriptor actionDescriptor,
            IWebApiRequestMessage request,
            Func <Type, IEdmModel> modelFunction,
            Func <ODataQueryContext, IODataQueryOptions> createQueryOptionFunction,
            Action <HttpStatusCode> createResponseAction,
            Action <HttpStatusCode, string, Exception> createErrorAction)
        {
            if (!_querySettings.PageSize.HasValue && responseValue != null)
            {
                GetModelBoundPageSize(responseValue, singleResultCollection, actionDescriptor, modelFunction, request.Context.Path, createErrorAction);
            }

            // Apply the query if there are any query options, if there is a page size set, in the case of
            // SingleResult or in the case of $count request.
            bool shouldApplyQuery = responseValue != null &&
                                    request.RequestUri != null &&
                                    (!String.IsNullOrWhiteSpace(request.RequestUri.Query) ||
                                     _querySettings.PageSize.HasValue ||
                                     _querySettings.ModelBoundPageSize.HasValue ||
                                     singleResultCollection != null ||
                                     request.IsCountRequest() ||
                                     ContainsAutoSelectExpandProperty(responseValue, singleResultCollection, actionDescriptor, modelFunction, request.Context.Path));

            object returnValue = null;

            if (shouldApplyQuery)
            {
                try
                {
                    object queryResult = ExecuteQuery(responseValue, singleResultCollection, actionDescriptor, modelFunction, request, createQueryOptionFunction);
                    if (queryResult == null && (request.Context.Path == null || singleResultCollection != null))
                    {
                        // This is the case in which a regular OData service uses the EnableQuery attribute.
                        // For OData services ODataNullValueMessageHandler should be plugged in for the service
                        // if this behavior is desired.
                        // For non OData services this behavior is equivalent as the one in the v3 version in order
                        // to reduce the friction when they decide to move to use the v4 EnableQueryAttribute.
                        createResponseAction(HttpStatusCode.NotFound);
                    }

                    returnValue = queryResult;
                }
                catch (ArgumentOutOfRangeException e)
                {
                    createErrorAction(
                        HttpStatusCode.BadRequest,
                        Error.Format(SRResources.QueryParameterNotSupported, e.Message),
                        e);
                }
                catch (NotImplementedException e)
                {
                    createErrorAction(
                        HttpStatusCode.BadRequest,
                        Error.Format(SRResources.UriQueryStringInvalid, e.Message),
                        e);
                }
                catch (NotSupportedException e)
                {
                    createErrorAction(
                        HttpStatusCode.BadRequest,
                        Error.Format(SRResources.UriQueryStringInvalid, e.Message),
                        e);
                }
                catch (InvalidOperationException e)
                {
                    // Will also catch ODataException here because ODataException derives from InvalidOperationException.
                    createErrorAction(
                        HttpStatusCode.BadRequest,
                        Error.Format(SRResources.UriQueryStringInvalid, e.Message),
                        e);
                }
            }

            return(returnValue);
        }