Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

namespace Microsoft.AspNetCore.OData.Abstracts
{

/// <summary>
/// Provide the interface for the details of a given OData request.
/// </summary>
Expand Down Expand Up @@ -99,7 +98,5 @@ public interface IODataFeature
/// </summary>
/// <value>Initially an empty <c>IDictionary&lt;string, object&gt;</c>.</value>
IDictionary<string, object> RoutingConventionsStore { get; }

}

}
12 changes: 10 additions & 2 deletions src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8225,6 +8225,13 @@
</summary>
<param name="actionExecutingContext">The action executing context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateQueryOptionsOnExecuting(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext)">
<summary>
Creates the <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/> for action executing validation.
</summary>
<param name="actionExecutingContext">The action executing context.</param>
<returns>The created <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/> or null if we can't create it during action executing.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext)">
<summary>
Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the
Expand Down Expand Up @@ -8315,11 +8322,12 @@
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateAndValidateQueryOptions(Microsoft.AspNetCore.Http.HttpRequest,Microsoft.AspNetCore.OData.Query.ODataQueryContext)">
<summary>
Create and validate a new instance of <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/> from a query and context.
Create and validate a new instance of <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/> from a query and context during action executed.
Developers can override this virtual method to provide its own <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/>.
</summary>
<param name="request">The incoming request.</param>
<param name="queryContext">The query context.</param>
<returns></returns>
<returns>The created <see cref="T:Microsoft.AspNetCore.OData.Query.ODataQueryOptions"/>.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.SingleOrDefault(System.Linq.IQueryable,Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)">
<summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 76 additions & 48 deletions src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/// <summary>
/// Creates the <see cref="ODataQueryOptions"/> for action executing validation.
/// </summary>
/// <param name="actionExecutingContext">The action executing context.</param>
/// <returns>The created <see cref="ODataQueryOptions"/> or null if we can't create it during action executing.</returns>
protected virtual ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext)
{
if (actionExecutingContext == null)
{
throw new ArgumentNullException(nameof(actionExecutingContext));
}

HttpRequest request = actionExecutingContext.HttpContext.Request;
ODataPath path = request.ODataFeature().Path;

Expand All @@ -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));
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -533,17 +549,29 @@ public virtual object ApplyQuery(object entity, ODataQueryOptions queryOptions)
}

/// <summary>
/// Create and validate a new instance of <see cref="ODataQueryOptions"/> from a query and context.
/// Create and validate a new instance of <see cref="ODataQueryOptions"/> from a query and context during action executed.
/// Developers can override this virtual method to provide its own <see cref="ODataQueryOptions"/>.
/// </summary>
/// <param name="request">The incoming request.</param>
/// <param name="queryContext">The query context.</param>
/// <returns></returns>
private ODataQueryOptions CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext)
/// <returns>The created <see cref="ODataQueryOptions"/>.</returns>
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -1278,6 +1304,28 @@ private static HttpActionExecutedContext GetActionExecutedContext<TResponse>(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<IFilterMetadata>(),
new Dictionary<string, object>(),
controller: new object());
}

private static ActionContext CreateActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}

private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
Expand Down