From a3cba39588f952377d2aace17bca89cd3c0a39df Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Fri, 5 Nov 2021 10:23:31 -0700 Subject: [PATCH] Add AddODataQueryFilter extension methods --- .../Microsoft.AspNetCore.OData.xml | 69 +++++++- .../ODataApplicationBuilderExtensions.cs | 2 +- .../ODataServiceCollectionExtensions.cs | 39 ++++- .../PublicAPI.Unshipped.txt | 9 + .../Query/QueryFilterProvider.cs | 160 ++++++++++++++++++ ...rosoft.AspNetCore.OData.PublicApi.Net5.bsl | 25 +++ ...t.AspNetCore.OData.PublicApi.NetCore31.bsl | 25 +++ 7 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index fe0af784f..e670aba76 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -5932,7 +5932,7 @@ Use OData route debug middleware. You can send request "~/$odata" after enabling this middleware. The to use. - + The . @@ -6213,6 +6213,27 @@ Provides extension methods to add OData services. + + + Enables query support for actions with an or return + type. To avoid processing unexpected or malicious queries, use the validation settings on + to validate incoming queries. For more information, visit + http://go.microsoft.com/fwlink/?LinkId=279712. + + The services collection. + The so that additional calls can be chained. + + + + Enables query support for actions with an or return + type. To avoid processing unexpected or malicious queries, use the validation settings on + to validate incoming queries. For more information, visit + http://go.microsoft.com/fwlink/?LinkId=279712. + + The services collection. + The action filter that executes the query. + The so that additional calls can be chained. + Adds the core OData services required for OData requests. @@ -9619,6 +9640,52 @@ The node to be translated. The translated node. + + + An implementation of that applies an action filter to + any action with an or return type + that doesn't bind a parameter of type . + + + + + Initializes a new instance of the class. + + The action filter that executes the query. + + + + Gets the action filter that executes the query. + + + + + Gets the order value for determining the order of execution of providers. Providers + execute in ascending numeric value of the Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + property. + + + + + Provides filters to apply to the specified action. + + The filter context. + + + + Summary: + Called in decreasing Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, + after all Microsoft.AspNetCore.Mvc.Filters.IFilterProviders have executed once. + + The Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext. + + + + Determines whether the given type is IQueryable. + + The type + true if the type is IQueryable. + This defines a $apply OData query option for querying. diff --git a/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs index 6266c5040..560c9f1be 100644 --- a/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs @@ -54,7 +54,7 @@ public static IApplicationBuilder UseODataQueryRequest(this IApplicationBuilder /// Use OData route debug middleware. You can send request "~/$odata" after enabling this middleware. /// /// The to use. - /// + /// The . public static IApplicationBuilder UseODataRouteDebug(this IApplicationBuilder app) { return app.UseODataRouteDebug(DefaultODataRouteDebugMiddlewareRoutePattern); diff --git a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs index 1be078634..4481291fa 100644 --- a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs @@ -5,8 +5,10 @@ // //------------------------------------------------------------------------------ +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Parser; @@ -22,14 +24,47 @@ namespace Microsoft.AspNetCore.OData /// /// Provides extension methods to add OData services. /// - internal static class ODataServiceCollectionExtensions + public static class ODataServiceCollectionExtensions { + /// + /// Enables query support for actions with an or return + /// type. To avoid processing unexpected or malicious queries, use the validation settings on + /// to validate incoming queries. For more information, visit + /// http://go.microsoft.com/fwlink/?LinkId=279712. + /// + /// The services collection. + /// The so that additional calls can be chained. + public static IServiceCollection AddODataQueryFilter(this IServiceCollection services) + { + return AddODataQueryFilter(services, new EnableQueryAttribute()); + } + + /// + /// Enables query support for actions with an or return + /// type. To avoid processing unexpected or malicious queries, use the validation settings on + /// to validate incoming queries. For more information, visit + /// http://go.microsoft.com/fwlink/?LinkId=279712. + /// + /// The services collection. + /// The action filter that executes the query. + /// The so that additional calls can be chained. + public static IServiceCollection AddODataQueryFilter(this IServiceCollection services, IActionFilter queryFilter) + { + if (services == null) + { + throw Error.ArgumentNull(nameof(services)); + } + + services.TryAddEnumerable(ServiceDescriptor.Singleton(new QueryFilterProvider(queryFilter))); + return services; + } + /// /// Adds the core OData services required for OData requests. /// /// The to add the services to. /// The so that additional calls can be chained. - public static IServiceCollection AddODataCore(this IServiceCollection services) + internal static IServiceCollection AddODataCore(this IServiceCollection services) { if (services == null) { diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 84973fae7..63e9d330e 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -676,6 +676,7 @@ Microsoft.AspNetCore.OData.ODataOptions.UrlKeyDelimiter.set -> void Microsoft.AspNetCore.OData.ODataOptionsSetup Microsoft.AspNetCore.OData.ODataOptionsSetup.Configure(Microsoft.AspNetCore.OData.ODataOptions options) -> void Microsoft.AspNetCore.OData.ODataOptionsSetup.ODataOptionsSetup(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.OData.Routing.Parser.IODataPathTemplateParser parser) -> void +Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions Microsoft.AspNetCore.OData.ODataUriFunctions Microsoft.AspNetCore.OData.Query.AllowedArithmeticOperators Microsoft.AspNetCore.OData.Query.AllowedArithmeticOperators.Add = 1 -> Microsoft.AspNetCore.OData.Query.AllowedArithmeticOperators @@ -964,6 +965,12 @@ Microsoft.AspNetCore.OData.Query.OrderByQueryOption.RawValue.get -> string Microsoft.AspNetCore.OData.Query.OrderByQueryOption.Validate(Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings validationSettings) -> void Microsoft.AspNetCore.OData.Query.OrderByQueryOption.Validator.get -> Microsoft.AspNetCore.OData.Query.Validator.OrderByQueryValidator Microsoft.AspNetCore.OData.Query.OrderByQueryOption.Validator.set -> void +Microsoft.AspNetCore.OData.Query.QueryFilterProvider +Microsoft.AspNetCore.OData.Query.QueryFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) -> void +Microsoft.AspNetCore.OData.Query.QueryFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) -> void +Microsoft.AspNetCore.OData.Query.QueryFilterProvider.Order.get -> int +Microsoft.AspNetCore.OData.Query.QueryFilterProvider.QueryFilter.get -> Microsoft.AspNetCore.Mvc.Filters.IActionFilter +Microsoft.AspNetCore.OData.Query.QueryFilterProvider.QueryFilterProvider(Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) -> void Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(object entity, Microsoft.AspNetCore.OData.Query.ODataQuerySettings settings) -> object Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption.ApplyTo(System.Linq.IQueryable queryable, Microsoft.AspNetCore.OData.Query.ODataQuerySettings settings) -> System.Linq.IQueryable @@ -1530,6 +1537,8 @@ static Microsoft.AspNetCore.OData.ODataMvcBuilderExtensions.AddOData(this Micros static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddODataQueryFilter(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddODataQueryFilter(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.AspNetCore.OData.ODataUriFunctions.AddCustomUriFunction(string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) -> void static Microsoft.AspNetCore.OData.ODataUriFunctions.RemoveCustomUriFunction(string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) -> bool static Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateErrorResponse(string message, System.Exception exception = null) -> Microsoft.AspNetCore.Mvc.SerializableError diff --git a/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs b/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs new file mode 100644 index 000000000..40f7c7252 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.OData.Common; +using Microsoft.AspNetCore.OData.Results; + +namespace Microsoft.AspNetCore.OData.Query +{ + /// + /// An implementation of that applies an action filter to + /// any action with an or return type + /// that doesn't bind a parameter of type . + /// + public class QueryFilterProvider : IFilterProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The action filter that executes the query. + public QueryFilterProvider(IActionFilter queryFilter) + { + if (queryFilter == null) + { + throw Error.ArgumentNull(nameof(queryFilter)); + } + + QueryFilter = queryFilter; + } + + /// + /// Gets the action filter that executes the query. + /// + public IActionFilter QueryFilter { get; } + + /// + /// Gets the order value for determining the order of execution of providers. Providers + /// execute in ascending numeric value of the Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + /// property. + /// + public int Order + { + get + { + // Providers are executed in an ordering determined by an ascending sort of the + // Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order property. A provider with + // a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // called before that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. + // The Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // method is called in the reverse ordering after all calls to Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext). + // A provider with a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // method called after that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. + // If two providers have the same numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, + // then their relative execution order is undefined. + return 0; + } + } + + /// + /// Provides filters to apply to the specified action. + /// + /// The filter context. + public void OnProvidersExecuting(FilterProviderContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + // Actions with a bound parameter of type ODataQueryOptions do not support the query filter + // The assumption is that the action will handle the querying within the action implementation + ControllerActionDescriptor controllerActionDescriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor != null) + { + Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; + if (ShouldAddFilter(context, returnType, controllerActionDescriptor)) + { + var filterDesc = new FilterDescriptor(QueryFilter, FilterScope.Global); + context.Results.Add(new FilterItem(filterDesc, QueryFilter)); + } + } + } + + /// + /// Summary: + /// Called in decreasing Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, + /// after all Microsoft.AspNetCore.Mvc.Filters.IFilterProviders have executed once. + /// + /// The Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext. + public void OnProvidersExecuted(FilterProviderContext context) + { + } + + private bool ShouldAddFilter(FilterProviderContext context, Type returnType, ControllerActionDescriptor controllerActionDescriptor) + { + if (returnType == null) + { + return false; + } + + // Get the inner return type if type is a task. + Type innerReturnType = returnType; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + innerReturnType = returnType.GetGenericArguments().First(); + } + + // See if this type is a SingleResult or is derived from SingleResult. + bool isSingleResult = false; + if (innerReturnType.IsGenericType) + { + Type genericType = innerReturnType.GetGenericTypeDefinition(); + Type baseType = innerReturnType.BaseType; + isSingleResult = (genericType == typeof(SingleResult<>) || baseType == typeof(SingleResult)); + } + + // Don't apply the filter if the result is not IQueryable() or SingleReult(). + if (!IsIQueryable(innerReturnType) && !isSingleResult) + { + return false; + } + + // If the controller takes a ODataQueryOptions, don't apply the filter. + if (controllerActionDescriptor.Parameters + .Any(parameter => TypeHelper.IsTypeAssignableFrom(typeof(ODataQueryOptions), parameter.ParameterType))) + { + return false; + } + + // Don't apply a global filter if one of the same type exists. + if (context.Results.Where(f => f.Filter?.GetType() == QueryFilter.GetType()).Any()) + { + return false; + } + + return true; + } + + /// + /// Determines whether the given type is IQueryable. + /// + /// The type + /// true if the type is IQueryable. + internal static bool IsIQueryable(Type type) + { + return type == typeof(IQueryable) || + (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IQueryable<>)); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl index 63a459dc6..8126eff55 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl @@ -63,6 +63,21 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { public static Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder AddOData (Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action`2[[Microsoft.AspNetCore.OData.ODataOptions],[System.IServiceProvider]] setupAction) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) +} + public sealed class Microsoft.AspNetCore.OData.ODataUriFunctions { public static void AddCustomUriFunction (string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) public static bool RemoveCustomUriFunction (string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) @@ -1458,6 +1473,16 @@ public class Microsoft.AspNetCore.OData.Query.OrderByQueryOption { public void Validate (Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings validationSettings) } +public class Microsoft.AspNetCore.OData.Query.QueryFilterProvider : IFilterProvider { + public QueryFilterProvider (Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) + + int Order { public virtual get; } + Microsoft.AspNetCore.Mvc.Filters.IActionFilter QueryFilter { public get; } + + public virtual void OnProvidersExecuted (Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) + public virtual void OnProvidersExecuting (Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) +} + public class Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption { public SelectExpandQueryOption (string select, string expand, Microsoft.AspNetCore.OData.Query.ODataQueryContext context, Microsoft.OData.UriParser.ODataQueryOptionParser queryOptionParser) 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 63a459dc6..8126eff55 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 @@ -63,6 +63,21 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { public static Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder AddOData (Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action`2[[Microsoft.AspNetCore.OData.ODataOptions],[System.IServiceProvider]] setupAction) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) +} + public sealed class Microsoft.AspNetCore.OData.ODataUriFunctions { public static void AddCustomUriFunction (string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) public static bool RemoveCustomUriFunction (string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) @@ -1458,6 +1473,16 @@ public class Microsoft.AspNetCore.OData.Query.OrderByQueryOption { public void Validate (Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings validationSettings) } +public class Microsoft.AspNetCore.OData.Query.QueryFilterProvider : IFilterProvider { + public QueryFilterProvider (Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) + + int Order { public virtual get; } + Microsoft.AspNetCore.Mvc.Filters.IActionFilter QueryFilter { public get; } + + public virtual void OnProvidersExecuted (Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) + public virtual void OnProvidersExecuting (Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext context) +} + public class Microsoft.AspNetCore.OData.Query.SelectExpandQueryOption { public SelectExpandQueryOption (string select, string expand, Microsoft.AspNetCore.OData.Query.ODataQueryContext context, Microsoft.OData.UriParser.ODataQueryOptionParser queryOptionParser)