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();