PageRenderTime 24ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/src/System.Web.Http.OData/QueryableAttribute.cs

http://aspnetwebstack.codeplex.com
C# | 534 lines | 344 code | 48 blank | 142 comment | 55 complexity | 635adf1b5d719d97dbde9ba304856aa6 MD5 | raw file
  1. // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Diagnostics.Contracts;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Http;
  9. using System.Web.Http.Controllers;
  10. using System.Web.Http.Filters;
  11. using System.Web.Http.OData;
  12. using System.Web.Http.OData.Formatter;
  13. using System.Web.Http.OData.Properties;
  14. using System.Web.Http.OData.Query;
  15. using Microsoft.Data.Edm;
  16. using Microsoft.Data.OData;
  17. namespace System.Web.Http
  18. {
  19. /// <summary>
  20. /// This class defines an attribute that can be applied to an action to enable querying using the OData query syntax.
  21. /// To avoid processing unexpected or malicious queries, use the validation settings on <see cref="QueryableAttribute"/> to validate
  22. /// incoming queries. For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
  23. /// </summary>
  24. [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want to be able to subclass this type.")]
  25. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
  26. public class QueryableAttribute : ActionFilterAttribute
  27. {
  28. private const char CommaSeparator = ',';
  29. // validation settings
  30. private ODataValidationSettings _validationSettings;
  31. private string _allowedOrderByProperties;
  32. // query settings
  33. private ODataQuerySettings _querySettings;
  34. /// <summary>
  35. /// Enables a controller action to support OData query parameters.
  36. /// </summary>
  37. public QueryableAttribute()
  38. {
  39. _validationSettings = new ODataValidationSettings();
  40. _querySettings = new ODataQuerySettings();
  41. }
  42. /// <summary>
  43. /// Gets or sets a value indicating whether query composition should
  44. /// alter the original query when necessary to ensure a stable sort order.
  45. /// </summary>
  46. /// <value>A <c>true</c> value indicates the original query should
  47. /// be modified when necessary to guarantee a stable sort order.
  48. /// A <c>false</c> value indicates the sort order can be considered
  49. /// stable without modifying the query. Query providers that ensure
  50. /// a stable sort order should set this value to <c>false</c>.
  51. /// The default value is <c>true</c>.</value>
  52. public bool EnsureStableOrdering
  53. {
  54. get
  55. {
  56. return _querySettings.EnsureStableOrdering;
  57. }
  58. set
  59. {
  60. _querySettings.EnsureStableOrdering = value;
  61. }
  62. }
  63. /// <summary>
  64. /// Gets or sets a value indicating how null propagation should
  65. /// be handled during query composition.
  66. /// </summary>
  67. /// <value>
  68. /// The default is <see cref="HandleNullPropagationOption.Default"/>.
  69. /// </value>
  70. public HandleNullPropagationOption HandleNullPropagation
  71. {
  72. get
  73. {
  74. return _querySettings.HandleNullPropagation;
  75. }
  76. set
  77. {
  78. _querySettings.HandleNullPropagation = value;
  79. }
  80. }
  81. /// <summary>
  82. /// Gets or sets a value indicating whether constants should be parameterized. Parameterizing constants
  83. /// would result in better performance with Entity framework.
  84. /// </summary>
  85. /// <value>The default value is <c>true</c>.</value>
  86. public bool EnableConstantParameterization
  87. {
  88. get
  89. {
  90. return _querySettings.EnableConstantParameterization;
  91. }
  92. set
  93. {
  94. _querySettings.EnableConstantParameterization = value;
  95. }
  96. }
  97. /// <summary>
  98. /// Gets or sets the maximum depth of the Any or All elements nested inside the query.
  99. /// </summary>
  100. /// <remarks>
  101. /// This limit helps prevent Denial of Service attacks. The default value is 1.
  102. /// </remarks>
  103. /// <value>
  104. /// The maxiumum depth of the Any or All elements nested inside the query.
  105. /// </value>
  106. public int MaxAnyAllExpressionDepth
  107. {
  108. get
  109. {
  110. return _validationSettings.MaxAnyAllExpressionDepth;
  111. }
  112. set
  113. {
  114. _validationSettings.MaxAnyAllExpressionDepth = value;
  115. }
  116. }
  117. /// <summary>
  118. /// Gets or sets the maximum number of nodes inside the $filter syntax tree.
  119. /// </summary>
  120. /// <remarks>
  121. /// The default value is 100.
  122. /// </remarks>
  123. public int MaxNodeCount
  124. {
  125. get
  126. {
  127. return _validationSettings.MaxNodeCount;
  128. }
  129. set
  130. {
  131. _validationSettings.MaxNodeCount = value;
  132. }
  133. }
  134. /// <summary>
  135. /// Gets or sets the maximum number of query results to send back to clients.
  136. /// </summary>
  137. /// <value>
  138. /// The maximum number of query results to send back to clients.
  139. /// </value>
  140. public int PageSize
  141. {
  142. get
  143. {
  144. return _querySettings.PageSize ?? default(int);
  145. }
  146. set
  147. {
  148. _querySettings.PageSize = value;
  149. }
  150. }
  151. /// <summary>
  152. /// Gets or sets the query parameters that are allowed in queries. The default is all query options,
  153. /// including $filter, $skip, $top, $orderby, $expand, $select, $inlineCount, $format and $skiptoken.
  154. /// </summary>
  155. public AllowedQueryOptions AllowedQueryOptions
  156. {
  157. get
  158. {
  159. return _validationSettings.AllowedQueryOptions;
  160. }
  161. set
  162. {
  163. _validationSettings.AllowedQueryOptions = value;
  164. }
  165. }
  166. /// <summary>
  167. /// Gets or sets a value that represents a list of allowed functions used in the $filter query.
  168. ///
  169. /// The allowed functions includes the following:
  170. ///
  171. /// String related: substringof, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat
  172. ///
  173. /// e.g. ~/Customers?$filter=length(CompanyName) eq 19
  174. ///
  175. /// DateTime related: year, years, month, months, day, days, hour, hours, minute, minutes, second, seconds
  176. ///
  177. /// e.g. ~/Employees?$filter=year(BirthDate) eq 1971
  178. ///
  179. /// Math related: round, floor, ceiling
  180. ///
  181. /// Type related:isof, cast,
  182. ///
  183. /// Collection related: any, all
  184. ///
  185. /// </summary>
  186. public AllowedFunctions AllowedFunctions
  187. {
  188. get
  189. {
  190. return _validationSettings.AllowedFunctions;
  191. }
  192. set
  193. {
  194. _validationSettings.AllowedFunctions = value;
  195. }
  196. }
  197. /// <summary>
  198. /// Gets or sets a value that represents a list of allowed arithmetic operators including 'add', 'sub', 'mul', 'div', 'mod'.
  199. /// </summary>
  200. public AllowedArithmeticOperators AllowedArithmeticOperators
  201. {
  202. get
  203. {
  204. return _validationSettings.AllowedArithmeticOperators;
  205. }
  206. set
  207. {
  208. _validationSettings.AllowedArithmeticOperators = value;
  209. }
  210. }
  211. /// <summary>
  212. /// Gets or sets a value that represents a list of allowed logical Operators such as 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or', 'not'.
  213. /// </summary>
  214. public AllowedLogicalOperators AllowedLogicalOperators
  215. {
  216. get
  217. {
  218. return _validationSettings.AllowedLogicalOperators;
  219. }
  220. set
  221. {
  222. _validationSettings.AllowedLogicalOperators = value;
  223. }
  224. }
  225. /// <summary>
  226. /// Gets or sets a string with comma seperated list of property names. The queryable result can only be ordered by
  227. /// those properties defined in this list.
  228. ///
  229. /// Note, by default this string is null, which means it can be ordered by any property.
  230. ///
  231. /// For example, setting this value to null or empty string means that we allow ordering the queryable result by any properties.
  232. /// Setting this value to "Name" means we only allow queryable result to be ordered by Name property.
  233. /// </summary>
  234. public string AllowedOrderByProperties
  235. {
  236. get
  237. {
  238. return _allowedOrderByProperties;
  239. }
  240. set
  241. {
  242. _allowedOrderByProperties = value;
  243. if (String.IsNullOrEmpty(value))
  244. {
  245. _validationSettings.AllowedOrderByProperties.Clear();
  246. }
  247. else
  248. {
  249. // now parse the value and set it to validationSettings
  250. string[] properties = _allowedOrderByProperties.Split(CommaSeparator);
  251. for (int i = 0; i < properties.Length; i++)
  252. {
  253. _validationSettings.AllowedOrderByProperties.Add(properties[i].Trim());
  254. }
  255. }
  256. }
  257. }
  258. /// <summary>
  259. /// Gets or sets the max value of $skip that a client can request.
  260. /// </summary>
  261. public int MaxSkip
  262. {
  263. get
  264. {
  265. return _validationSettings.MaxSkip ?? default(int);
  266. }
  267. set
  268. {
  269. _validationSettings.MaxSkip = value;
  270. }
  271. }
  272. /// <summary>
  273. /// Gets or sets the max value of $top that a client can request.
  274. /// </summary>
  275. public int MaxTop
  276. {
  277. get
  278. {
  279. return _validationSettings.MaxTop ?? default(int);
  280. }
  281. set
  282. {
  283. _validationSettings.MaxTop = value;
  284. }
  285. }
  286. /// <summary>
  287. /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the returning response message.
  288. /// It then validates the query from uri based on the validation settings on QueryableAttribute. It finally applies the query appropriately,
  289. /// and reset it back on the response message.
  290. /// </summary>
  291. /// <param name="actionExecutedContext">The context related to this action, including the response message, request message and HttpConfiguration etc.</param>
  292. public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
  293. {
  294. if (actionExecutedContext == null)
  295. {
  296. throw Error.ArgumentNull("actionExecutedContext");
  297. }
  298. HttpRequestMessage request = actionExecutedContext.Request;
  299. if (request == null)
  300. {
  301. throw Error.Argument("actionExecutedContext", SRResources.ActionExecutedContextMustHaveRequest);
  302. }
  303. HttpConfiguration configuration = request.GetConfiguration();
  304. if (configuration == null)
  305. {
  306. throw Error.Argument("actionExecutedContext", SRResources.RequestMustContainConfiguration);
  307. }
  308. if (actionExecutedContext.ActionContext == null)
  309. {
  310. throw Error.Argument("actionExecutedContext", SRResources.ActionExecutedContextMustHaveActionContext);
  311. }
  312. HttpActionDescriptor actionDescriptor = actionExecutedContext.ActionContext.ActionDescriptor;
  313. if (actionDescriptor == null)
  314. {
  315. throw Error.Argument("actionExecutedContext", SRResources.ActionContextMustHaveDescriptor);
  316. }
  317. HttpResponseMessage response = actionExecutedContext.Response;
  318. if (response != null && response.IsSuccessStatusCode)
  319. {
  320. ObjectContent responseContent = response.Content as ObjectContent;
  321. if (responseContent == null)
  322. {
  323. throw Error.Argument("actionExecutedContext", SRResources.QueryingRequiresObjectContent, response.Content.GetType().FullName);
  324. }
  325. ValidateReturnType(responseContent.ObjectType, actionDescriptor);
  326. // Apply the query if there are any query options or if there is a page size set
  327. if (responseContent.Value != null && request.RequestUri != null &&
  328. (!String.IsNullOrWhiteSpace(request.RequestUri.Query) || _querySettings.PageSize.HasValue))
  329. {
  330. try
  331. {
  332. IEnumerable query = responseContent.Value as IEnumerable;
  333. Contract.Assert(query != null, "ValidateResponseContent should have ensured the responseContent implements IEnumerable");
  334. IQueryable queryResults = ExecuteQuery(query, request, actionDescriptor);
  335. responseContent.Value = queryResults;
  336. }
  337. catch (ODataException e)
  338. {
  339. actionExecutedContext.Response = request.CreateErrorResponse(
  340. HttpStatusCode.BadRequest,
  341. SRResources.UriQueryStringInvalid,
  342. e);
  343. return;
  344. }
  345. }
  346. }
  347. }
  348. /// <summary>
  349. /// Validates the OData query in the incoming request.
  350. /// </summary>
  351. /// <param name="request">The incoming request.</param>
  352. /// <param name="queryOptions">The <see cref="ODataQueryOptions"/> instance constructed based on the incoming request.</param>
  353. /// <remarks>
  354. /// Override this method to perform additional validation of the query. By default, the implementation
  355. /// throws an exception if the query contains unsupported query parameters.
  356. /// </remarks>
  357. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Response disposed after being sent.")]
  358. public virtual void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
  359. {
  360. if (request == null)
  361. {
  362. throw Error.ArgumentNull("request");
  363. }
  364. if (queryOptions == null)
  365. {
  366. throw Error.ArgumentNull("queryOptions");
  367. }
  368. IEnumerable<KeyValuePair<string, string>> queryParameters = request.GetQueryNameValuePairs();
  369. foreach (KeyValuePair<string, string> kvp in queryParameters)
  370. {
  371. if (!ODataQueryOptions.IsSystemQueryOption(kvp.Key) &&
  372. kvp.Key.StartsWith("$", StringComparison.Ordinal))
  373. {
  374. // we don't support any custom query options that start with $
  375. throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest,
  376. Error.Format(SRResources.QueryParameterNotSupported, kvp.Key)));
  377. }
  378. }
  379. queryOptions.Validate(_validationSettings);
  380. }
  381. /// <summary>
  382. /// Applies the query to the given IQueryable based on incoming query from uri and query settings.
  383. /// </summary>
  384. /// <param name="queryable">The original queryable instance from the response message.</param>
  385. /// <param name="queryOptions">The <see cref="ODataQueryOptions"/> instance constructed based on the incoming request.</param>
  386. /// <remarks>
  387. /// Override this method to perform additional query composition of the query. By default, the implementation
  388. /// supports $top, $skip, $orderby and $filter.
  389. /// </remarks>
  390. public virtual IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
  391. {
  392. if (queryable == null)
  393. {
  394. throw Error.ArgumentNull("queryable");
  395. }
  396. if (queryOptions == null)
  397. {
  398. throw Error.ArgumentNull("queryOptions");
  399. }
  400. return queryOptions.ApplyTo(queryable, _querySettings);
  401. }
  402. private static void ValidateReturnType(Type responseContentType, HttpActionDescriptor actionDescriptor)
  403. {
  404. if (!IsSupportedReturnType(responseContentType))
  405. {
  406. throw Error.InvalidOperation(
  407. SRResources.InvalidReturnTypeForQuerying,
  408. actionDescriptor.ActionName,
  409. actionDescriptor.ControllerDescriptor.ControllerName,
  410. responseContentType.FullName);
  411. }
  412. }
  413. internal static bool IsSupportedReturnType(Type objectType)
  414. {
  415. Contract.Assert(objectType != null);
  416. if (objectType == typeof(IEnumerable) || objectType == typeof(IQueryable))
  417. {
  418. return true;
  419. }
  420. if (objectType.IsGenericType)
  421. {
  422. Type genericTypeDefinition = objectType.GetGenericTypeDefinition();
  423. if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(IQueryable<>))
  424. {
  425. return true;
  426. }
  427. }
  428. return false;
  429. }
  430. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Response disposed after being sent.")]
  431. private IQueryable ExecuteQuery(IEnumerable query, HttpRequestMessage request, HttpActionDescriptor actionDescriptor)
  432. {
  433. Type originalQueryType = query.GetType();
  434. Type elementClrType = TypeHelper.GetImplementedIEnumerableType(originalQueryType);
  435. if (elementClrType == null)
  436. {
  437. // The element type cannot be determined because the type of the content
  438. // is not IEnumerable<T> or IQueryable<T>.
  439. throw Error.InvalidOperation(
  440. SRResources.FailedToRetrieveTypeToBuildEdmModel,
  441. this.GetType().Name,
  442. actionDescriptor.ActionName,
  443. actionDescriptor.ControllerDescriptor.ControllerName,
  444. originalQueryType.FullName);
  445. }
  446. IEdmModel model = GetModel(elementClrType, request, actionDescriptor);
  447. if (model == null)
  448. {
  449. throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull);
  450. }
  451. ODataQueryContext queryContext = new ODataQueryContext(model, elementClrType);
  452. ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request);
  453. ValidateQuery(request, queryOptions);
  454. // apply the query
  455. IQueryable queryable = query as IQueryable;
  456. if (queryable == null)
  457. {
  458. queryable = query.AsQueryable();
  459. }
  460. return ApplyQuery(queryable, queryOptions);
  461. }
  462. /// <summary>
  463. /// Gets the EDM model for the given type and request.
  464. /// </summary>
  465. /// <param name="elementClrType">The CLR type to retrieve a model for.</param>
  466. /// <param name="request">The request message to retrieve a model for.</param>
  467. /// <param name="actionDescriptor">The action descriptor for the action being queried on.</param>
  468. /// <returns>The EDM model for the given type and request.</returns>
  469. /// <remarks>
  470. /// Override this method to customize the EDM model used for querying.
  471. /// </remarks>
  472. public virtual IEdmModel GetModel(Type elementClrType, HttpRequestMessage request, HttpActionDescriptor actionDescriptor)
  473. {
  474. // Get model for the request
  475. IEdmModel model = request.GetEdmModel();
  476. if (model == null || model.GetEdmType(elementClrType) == null)
  477. {
  478. // user has not configured anything or has registered a model without the element type
  479. // let's create one just for this type and cache it in the action descriptor
  480. model = actionDescriptor.GetEdmModel(elementClrType);
  481. }
  482. Contract.Assert(model != null);
  483. return model;
  484. }
  485. }
  486. }