diff options
author | youssefm <youssefm@microsoft.com> | 2012-10-12 00:58:28 +0400 |
---|---|---|
committer | youssefm <youssefm@microsoft.com> | 2012-10-12 05:38:45 +0400 |
commit | 29fa9e0f81ccbd59fdcfb7950a63a864d9be19cb (patch) | |
tree | f548b21907d58b8f3d80dc43cf9f06b8fbb8d958 /src | |
parent | 45c1223c1d410755b6948407ec70bb6818f15bf9 (diff) |
[OData] Add new HttpConfiguration.EnableQuerySupport to enable querying globally for the server
Also [Queryable] refatoring and adding an error message when the ObjectContent is not of ObjectType IEnumerable, IQueryable, IEnumerable<T>, or IQueryable<T>
Diffstat (limited to 'src')
6 files changed, 192 insertions, 52 deletions
diff --git a/src/System.Web.Http.OData/HttpConfigurationExtensions.cs b/src/System.Web.Http.OData/HttpConfigurationExtensions.cs index 9516fcaa..2aeb591d 100644 --- a/src/System.Web.Http.OData/HttpConfigurationExtensions.cs +++ b/src/System.Web.Http.OData/HttpConfigurationExtensions.cs @@ -2,9 +2,11 @@ using System.ComponentModel; using System.Linq; +using System.Web.Http.Filters; using System.Web.Http.OData; using System.Web.Http.OData.Formatter; using System.Web.Http.OData.Properties; +using System.Web.Http.OData.Query; using Microsoft.Data.Edm; namespace System.Web.Http @@ -170,5 +172,34 @@ namespace System.Web.Http } configuration.Properties[ODataActionResolverKey] = resolver; } + + /// <summary> + /// Enables query support for actions with an <see cref="IQueryable"/> or <see cref="IQueryable{T}"/> return type. + /// </summary> + /// <param name="configuration">The server configuration.</param> + public static void EnableQuerySupport(this HttpConfiguration configuration) + { + if (configuration == null) + { + throw Error.ArgumentNull("configuration"); + } + + configuration.Services.Add(typeof(IFilterProvider), new QueryableFilterProvider()); + } + + /// <summary> + /// Enables query support for actions with an <see cref="IQueryable"/> or <see cref="IQueryable{T}"/> return type. + /// </summary> + /// <param name="configuration">The server configuration.</param> + /// <param name="resultLimit">The maximum number of results to return.</param> + public static void EnableQuerySupport(this HttpConfiguration configuration, int resultLimit) + { + if (configuration == null) + { + throw Error.ArgumentNull("configuration"); + } + + configuration.Services.Add(typeof(IFilterProvider), new QueryableFilterProvider() { ResultLimit = resultLimit }); + } } } diff --git a/src/System.Web.Http.OData/OData/Query/QueryableFilterProvider.cs b/src/System.Web.Http.OData/OData/Query/QueryableFilterProvider.cs new file mode 100644 index 00000000..10b272e6 --- /dev/null +++ b/src/System.Web.Http.OData/OData/Query/QueryableFilterProvider.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +namespace System.Web.Http.OData.Query +{ + /// <summary> + /// An implementation of <see cref="IFilterProvider"/> that applies the <see cref="QueryableAttribute"/> to + /// any action with an <see cref="IQueryable"/> or <see cref="IQueryable{T}"/> return type that doesn't bind + /// a parameter of type <see cref="ODataQueryOptions"/>. + /// </summary> + public class QueryableFilterProvider : IFilterProvider + { + /// <summary> + /// Gets or sets the maximum number of query results to return. + /// </summary> + /// <value> + /// The maximum number of query results to return, or <c>null</c> if there is no limit. + /// </value> + public int? ResultLimit { get; set; } + + /// <summary> + /// Provides filters to apply to the specified action. + /// </summary> + /// <param name="configuration">The server configuration.</param> + /// <param name="actionDescriptor">The action descriptor for the action to provide filters for.</param> + /// <returns> + /// The filters to apply to the specified action. + /// </returns> + public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor) + { + // Actions with a bound parameter of type ODataQueryOptions do not support the [Queryable] attribute + // The assumption is that the action will handle the querying within the action implementation + if (actionDescriptor != null && IsIQueryable(actionDescriptor.ReturnType) && + !actionDescriptor.GetParameters().Any(parameter => parameter.ParameterType == typeof(ODataQueryOptions))) + { + QueryableAttribute filter = new QueryableAttribute(); + if (ResultLimit.HasValue) + { + filter.ResultLimit = ResultLimit.Value; + } + return new FilterInfo[] { new FilterInfo(filter, FilterScope.Global) }; + } + + return Enumerable.Empty<FilterInfo>(); + } + + private static bool IsIQueryable(Type type) + { + return type == typeof(IQueryable) || + (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IQueryable<>)); + } + } +} diff --git a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs index db2eb2f6..f40dfa62 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.Designer.cs +++ b/src/System.Web.Http.OData/Properties/SRResources.Designer.cs @@ -502,6 +502,15 @@ namespace System.Web.Http.OData.Properties { } /// <summary> + /// Looks up a localized string similar to The action '{0}' on controller '{1}' with return type '{2}' cannot support querying. Ensure the type of the returned content is IEnumerable, IQueryable, or a generic form of either interface.. + /// </summary> + internal static string InvalidReturnTypeForQuerying { + get { + return ResourceManager.GetString("InvalidReturnTypeForQuerying", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The item must be of type '{0}'.. /// </summary> internal static string ItemMustBeOfType { diff --git a/src/System.Web.Http.OData/Properties/SRResources.resx b/src/System.Web.Http.OData/Properties/SRResources.resx index 1f76d9a4..8344fa9c 100644 --- a/src/System.Web.Http.OData/Properties/SRResources.resx +++ b/src/System.Web.Http.OData/Properties/SRResources.resx @@ -438,4 +438,7 @@ <data name="BinaryOperatorNotSupported" xml:space="preserve"> <value>A binary operator with incompatible types was detected. Found operand types '{0}' and '{1}' for operator kind '{2}'.</value> </data> + <data name="InvalidReturnTypeForQuerying" xml:space="preserve"> + <value>The action '{0}' on controller '{1}' with return type '{2}' cannot support querying. Ensure the type of the returned content is IEnumerable, IQueryable, or a generic form of either interface.</value> + </data> </root>
\ No newline at end of file diff --git a/src/System.Web.Http.OData/QueryableAttribute.cs b/src/System.Web.Http.OData/QueryableAttribute.cs index f61809a2..ffc3d50b 100644 --- a/src/System.Web.Http.OData/QueryableAttribute.cs +++ b/src/System.Web.Http.OData/QueryableAttribute.cs @@ -118,65 +118,23 @@ namespace System.Web.Http HttpResponseMessage response = actionExecutedContext.Response; - IEnumerable query; - IQueryable queryable = null; - if (response != null && response.IsSuccessStatusCode && response.TryGetContentValue(out query)) + if (response != null && response.IsSuccessStatusCode) { + ObjectContent responseContent = response.Content as ObjectContent; + ValidateReturnType(responseContent.ObjectType, actionDescriptor); + // Apply the query if there are any query options or if there is a result limit set - if (request.RequestUri != null && (!String.IsNullOrWhiteSpace(request.RequestUri.Query) || _resultLimit.HasValue)) + if (responseContent != null && responseContent.Value != null && request.RequestUri != null && + (!String.IsNullOrWhiteSpace(request.RequestUri.Query) || _resultLimit.HasValue)) { ValidateQuery(request); try { - Type originalQueryType = query.GetType(); - Type entityClrType = TypeHelper.GetImplementedIEnumerableType(originalQueryType); - - if (entityClrType == 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, - this.GetType().Name, - actionDescriptor.ActionName, - actionDescriptor.ControllerDescriptor.ControllerName, - originalQueryType.FullName); - } - - ODataQueryContext queryContext = CreateQueryContext(entityClrType, configuration, actionDescriptor); - ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request); - - // Filter and OrderBy require entity sets. Top and Skip may accept primitives. - if (queryContext.IsPrimitiveClrType && (queryOptions.Filter != null || queryOptions.OrderBy != null)) - { - // An attempt to use a query option not allowed for primitive types - // generates a BadRequest with a general message that avoids information disclosure. - actionExecutedContext.Response = request.CreateErrorResponse( - HttpStatusCode.BadRequest, - SRResources.OnlySkipAndTopSupported); - return; - } - - // apply the query - queryable = query as IQueryable; - if (queryable == null) - { - queryable = query.AsQueryable(); - } - - ODataQuerySettings querySettings = new ODataQuerySettings - { - EnsureStableOrdering = EnsureStableOrdering, - HandleNullPropagation = HandleNullPropagation, - ResultLimit = _resultLimit - }; - - queryable = queryOptions.ApplyTo(queryable, querySettings); - - Contract.Assert(queryable != null); - // we don't support shape changing query composition - ((ObjectContent)response.Content).Value = queryable; + IEnumerable query = responseContent.Value as IEnumerable; + Contract.Assert(query != null, "ValidateResponseContent should have ensured the responseContent implements IEnumerable"); + IQueryable queryResults = ExecuteQuery(query, request, configuration, actionDescriptor); + responseContent.Value = queryResults; } catch (ODataException e) { @@ -190,6 +148,87 @@ namespace System.Web.Http } } + private static void ValidateReturnType(Type responseContentType, HttpActionDescriptor actionDescriptor) + { + if (!IsSupportedReturnType(responseContentType)) + { + throw Error.InvalidOperation( + SRResources.InvalidReturnTypeForQuerying, + actionDescriptor.ActionName, + actionDescriptor.ControllerDescriptor.ControllerName, + responseContentType.FullName); + } + } + + private static bool IsSupportedReturnType(Type objectType) + { + Contract.Assert(objectType != null); + + if (objectType == typeof(IEnumerable) || objectType == typeof(IQueryable)) + { + return true; + } + + if (objectType.IsGenericType) + { + Type genericTypeDefinition = objectType.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(IQueryable<>)) + { + return true; + } + } + + return false; + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Response disposed after being sent.")] + private IQueryable ExecuteQuery(IEnumerable query, HttpRequestMessage request, HttpConfiguration configuration, HttpActionDescriptor actionDescriptor) + { + Type originalQueryType = query.GetType(); + Type entityClrType = TypeHelper.GetImplementedIEnumerableType(originalQueryType); + + if (entityClrType == 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, + this.GetType().Name, + actionDescriptor.ActionName, + actionDescriptor.ControllerDescriptor.ControllerName, + originalQueryType.FullName); + } + + ODataQueryContext queryContext = CreateQueryContext(entityClrType, configuration, actionDescriptor); + ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request); + + // Filter and OrderBy require entity sets. Top and Skip may accept primitives. + if (queryContext.IsPrimitiveClrType && (queryOptions.Filter != null || queryOptions.OrderBy != null)) + { + // An attempt to use a query option not allowed for primitive types + // generates a BadRequest with a general message that avoids information disclosure. + throw new HttpResponseException(request.CreateErrorResponse( + HttpStatusCode.BadRequest, + SRResources.OnlySkipAndTopSupported)); + } + + // apply the query + IQueryable queryable = query as IQueryable; + if (queryable == null) + { + queryable = query.AsQueryable(); + } + + ODataQuerySettings querySettings = new ODataQuerySettings + { + EnsureStableOrdering = EnsureStableOrdering, + HandleNullPropagation = HandleNullPropagation, + ResultLimit = _resultLimit + }; + + return queryOptions.ApplyTo(queryable, querySettings); + } + private static ODataQueryContext CreateQueryContext(Type entityClrType, HttpConfiguration configuration, HttpActionDescriptor actionDescriptor) { // Primitive types do not construct an EDM model and deal only with the CLR Type diff --git a/src/System.Web.Http.OData/System.Web.Http.OData.csproj b/src/System.Web.Http.OData/System.Web.Http.OData.csproj index b2c65769..1abd50f1 100644 --- a/src/System.Web.Http.OData/System.Web.Http.OData.csproj +++ b/src/System.Web.Http.OData/System.Web.Http.OData.csproj @@ -199,6 +199,7 @@ <Compile Include="OData\Query\ODataRawQueryOptions.cs" /> <Compile Include="OData\Formatter\Serialization\ODataPayloadKindHelper.cs" /> <Compile Include="OData\ODataResult.cs" /> + <Compile Include="OData\Query\QueryableFilterProvider.cs" /> <Compile Include="OData\Query\TopQueryOption.cs" /> <Compile Include="OData\Delta.cs" /> <Compile Include="OData\ExpressionHelperMethods.cs" /> |