diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs b/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs index 9781e5a8f..ed9fd8f3d 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs @@ -16,7 +16,6 @@ namespace Microsoft.AspNetCore.OData.Abstracts { - /// /// Provide the interface for the details of a given OData request. /// @@ -99,7 +98,5 @@ public interface IODataFeature /// /// Initially an empty IDictionary<string, object>. IDictionary RoutingConventionsStore { get; } - } - } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index e900babf5..21e340416 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -8225,6 +8225,13 @@ The action executing context. + + + Creates the for action executing validation. + + The action executing context. + The created or null if we can't create it during action executing. + Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the @@ -8315,11 +8322,12 @@ - Create and validate a new instance of from a query and context. + Create and validate a new instance of from a query and context during action executed. + Developers can override this virtual method to provide its own . The incoming request. The query context. - + The created . diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index ae7f1bcaa..b2c777502 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -1826,6 +1826,8 @@ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerProvid virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerProvider.GetODataPayloadSerializer(System.Type type, Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializer virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(object entity, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) -> object virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(System.Linq.IQueryable queryable, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) -> System.Linq.IQueryable +virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateAndValidateQueryOptions(Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.OData.Query.ODataQueryContext queryContext) -> Microsoft.AspNetCore.OData.Query.ODataQueryOptions +virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateQueryOptionsOnExecuting(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext actionExecutingContext) -> Microsoft.AspNetCore.OData.Query.ODataQueryOptions virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.GetModel(System.Type elementClrType, Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor actionDescriptor) -> Microsoft.OData.Edm.IEdmModel virtual Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ValidateQuery(Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) -> void virtual Microsoft.AspNetCore.OData.Query.ETag.ApplyTo(System.Linq.IQueryable query) -> System.Linq.IQueryable diff --git a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs index 3cfe4ff86..2ac587e56 100644 --- a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs @@ -62,6 +62,60 @@ public override void OnActionExecuting(ActionExecutingContext actionExecutingCon actionExecutingContext.HttpContext.Items.TryAdd(nameof(RequestQueryData), requestQueryData); + ODataQueryOptions queryOptions = CreateQueryOptionsOnExecuting(actionExecutingContext); + if (queryOptions == null) + { + return; // skip validation + } + + // Create and validate the query options. + requestQueryData.QueryValidationRunBeforeActionExecution = true; + requestQueryData.ProcessedQueryOptions = queryOptions; + + try + { + HttpRequest request = actionExecutingContext.HttpContext.Request; + ValidateQuery(request, requestQueryData.ProcessedQueryOptions); + } + catch (ArgumentOutOfRangeException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.QueryParameterNotSupported, e.Message), + e); + } + catch (NotImplementedException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + catch (NotSupportedException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + catch (InvalidOperationException e) + { + // Will also catch ODataException here because ODataException derives from InvalidOperationException. + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + } + + /// + /// Creates the for action executing validation. + /// + /// The action executing context. + /// The created or null if we can't create it during action executing. + protected virtual ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) + { + if (actionExecutingContext == null) + { + throw new ArgumentNullException(nameof(actionExecutingContext)); + } + HttpRequest request = actionExecutingContext.HttpContext.Request; ODataPath path = request.ODataFeature().Path; @@ -88,7 +142,7 @@ public override void OnActionExecuting(ActionExecutingContext actionExecutingCon // For Swagger metadata request. elementType is null. if (elementType == null || edmModel == null) { - return; + return null; } Type clrType = edmModel.GetClrType(elementType.ToEdmTypeReference(isNullable: false)); @@ -103,7 +157,7 @@ public override void OnActionExecuting(ActionExecutingContext actionExecutingCon // In case where CLRType is missing, $count, $expand verifications cannot be done. // More importantly $expand required ODataQueryContext with clrType which cannot be done // If the model is untyped. Hence for such cases, letting the validation run post action. - return; + return null; } } else @@ -114,10 +168,9 @@ public override void OnActionExecuting(ActionExecutingContext actionExecutingCon // Like IActionResult, SingleResult. For such cases, the validation is run in OnActionExecuted // When we have the result. ControllerActionDescriptor controllerActionDescriptor = actionExecutingContext.ActionDescriptor as ControllerActionDescriptor; - if (controllerActionDescriptor == null) { - return; + return null; } Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; @@ -145,52 +198,15 @@ public override void OnActionExecuting(ActionExecutingContext actionExecutingCon } else { - return; + return null; } - IEdmModel edmModel = GetModel( - elementType, - request, - controllerActionDescriptor); - - queryContext = new ODataQueryContext( - edmModel, - elementType); + IEdmModel edmModel = GetModel(elementType, request, controllerActionDescriptor); + queryContext = new ODataQueryContext(edmModel, elementType); } // Create and validate the query options. - requestQueryData.QueryValidationRunBeforeActionExecution = true; - requestQueryData.ProcessedQueryOptions = new ODataQueryOptions(queryContext, request); - - try - { - ValidateQuery(request, requestQueryData.ProcessedQueryOptions); - } - catch (ArgumentOutOfRangeException e) - { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.QueryParameterNotSupported, e.Message), - e); - } - catch (NotImplementedException e) - { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); - } - catch (NotSupportedException e) - { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); - } - catch (InvalidOperationException e) - { - // Will also catch ODataException here because ODataException derives from InvalidOperationException. - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); - } + return new ODataQueryOptions(queryContext, request); } /// @@ -533,17 +549,29 @@ public virtual object ApplyQuery(object entity, ODataQueryOptions queryOptions) } /// - /// Create and validate a new instance of from a query and context. + /// Create and validate a new instance of from a query and context during action executed. + /// Developers can override this virtual method to provide its own . /// /// The incoming request. /// The query context. - /// - private ODataQueryOptions CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext) + /// The created . + protected virtual ODataQueryOptions CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext) { + if (request == null) + { + throw Error.ArgumentNull("request"); + } + + if (queryContext == null) + { + throw Error.ArgumentNull("queryContext"); + } + RequestQueryData requestQueryData = request.HttpContext.Items[nameof(RequestQueryData)] as RequestQueryData; if (requestQueryData != null && requestQueryData.QueryValidationRunBeforeActionExecution) { + // processed, just return the query option and skip validation. return requestQueryData.ProcessedQueryOptions; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl index 077b5a41a..a69149515 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl @@ -1317,7 +1317,9 @@ public class Microsoft.AspNetCore.OData.Query.EnableQueryAttribute : Microsoft.A public virtual System.Linq.IQueryable ApplyQuery (System.Linq.IQueryable queryable, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) public virtual object ApplyQuery (object entity, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) + protected virtual Microsoft.AspNetCore.OData.Query.ODataQueryOptions CreateAndValidateQueryOptions (Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.OData.Query.ODataQueryContext queryContext) public static Microsoft.AspNetCore.Mvc.SerializableError CreateErrorResponse (string message, params System.Exception exception) + protected virtual Microsoft.AspNetCore.OData.Query.ODataQueryOptions CreateQueryOptionsOnExecuting (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext actionExecutingContext) public virtual Microsoft.OData.Edm.IEdmModel GetModel (System.Type elementClrType, Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor actionDescriptor) public virtual void OnActionExecuted (Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext actionExecutedContext) public virtual void OnActionExecuting (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext actionExecutingContext) diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index 077b5a41a..a69149515 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -1317,7 +1317,9 @@ public class Microsoft.AspNetCore.OData.Query.EnableQueryAttribute : Microsoft.A public virtual System.Linq.IQueryable ApplyQuery (System.Linq.IQueryable queryable, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) public virtual object ApplyQuery (object entity, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions) + protected virtual Microsoft.AspNetCore.OData.Query.ODataQueryOptions CreateAndValidateQueryOptions (Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.OData.Query.ODataQueryContext queryContext) public static Microsoft.AspNetCore.Mvc.SerializableError CreateErrorResponse (string message, params System.Exception exception) + protected virtual Microsoft.AspNetCore.OData.Query.ODataQueryOptions CreateQueryOptionsOnExecuting (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext actionExecutingContext) public virtual Microsoft.OData.Edm.IEdmModel GetModel (System.Type elementClrType, Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor actionDescriptor) public virtual void OnActionExecuted (Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext actionExecutedContext) public virtual void OnActionExecuting (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext actionExecutingContext) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs index dd79825db..52b79d5f7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs @@ -234,7 +234,7 @@ public void MaxOrderByNodeCount_Property_RoundTrips() } [Fact] - public void OnActionExecuted_Throws_Null_Context() + public void OnActionExecuted_Throws_Null_ActionExecutedContext() { ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuted(null), "actionExecutedContext"); } @@ -355,13 +355,39 @@ public override void OnActionExecuted(ActionExecutedContext actionExecutedContex } } -#if NETCORE // Following functionality is only supported in NetCore. [Fact] - public void OnActionExecuting_Throws_Null_Context() + public void OnActionExecuting_Throws_Null_ActionExecutingContext() { - ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuting(null), "context"); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuting(null), "actionExecutingContext"); + } + + [Fact] + public void OnActionExecuting_Calls_CreateQueryOptionsOnExecuting() + { + // Arrange + OverridQueryOptionEnableQueryAttribute enableQueryAttribute = new OverridQueryOptionEnableQueryAttribute(); + + ActionExecutingContext context = CreateDefaultActionExecutingContext(); + + // Act + Assert.False(enableQueryAttribute.Called); // Guard + enableQueryAttribute.OnActionExecuting(context); + + // Assert + Assert.True(enableQueryAttribute.Called); + } + + private class OverridQueryOptionEnableQueryAttribute : EnableQueryAttribute + { + public bool Called { get; set; } = false; + + protected override ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) + { + Called = true; + return null; + } } -#endif #if false // TODO #939: Enable these test on AspNetCore. [Fact] @@ -1278,6 +1304,28 @@ private static HttpActionExecutedContext GetActionExecutedContext(str return actionExecutedContext; } #endif + private static ActionExecutedContext CreateActionExecutedContext(ActionExecutingContext context) + { + return new ActionExecutedContext(context, context.Filters, context.Controller) + { + Result = context.Result, + }; + } + + private static ActionExecutingContext CreateDefaultActionExecutingContext() + { + return new ActionExecutingContext( + CreateActionContext(), + new List(), + new Dictionary(), + controller: new object()); + } + + private static ActionContext CreateActionContext() + { + return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + } + private static IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder();