/// <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)); }
/// <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); }
/// <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); }
/// <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); }
/// <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); }