diff --git a/sample/ODataRoutingSample/ExampleQueryOptionsBindingExtension.cs b/sample/ODataRoutingSample/ExampleQueryOptionsBindingExtension.cs new file mode 100644 index 000000000..f63104844 --- /dev/null +++ b/sample/ODataRoutingSample/ExampleQueryOptionsBindingExtension.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Query.Extension; +using System.Linq; + +namespace ODataRoutingSample +{ + public class ExampleQueryOptionsBindingExtension : IODataQueryOptionsBindingExtension + { + public IQueryable ApplyTo(IQueryable query, ODataQueryOptions queryOptions, ODataQuerySettings querySettings) + { + //Do something here + return query; + } + } +} diff --git a/sample/ODataRoutingSample/Startup.cs b/sample/ODataRoutingSample/Startup.cs index 43a4dab2f..d2b13b3ef 100644 --- a/sample/ODataRoutingSample/Startup.cs +++ b/sample/ODataRoutingSample/Startup.cs @@ -16,11 +16,9 @@ using Microsoft.AspNetCore.OData; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.OData.Batch; -using Microsoft.OData; using ODataRoutingSample.Models; using Microsoft.AspNetCore.Mvc.Controllers; using System.Reflection; -using System.Linq; namespace ODataRoutingSample { @@ -76,7 +74,10 @@ public void ConfigureServices(IServiceCollection services) */ .AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) .AddRouteComponents(model0) - .AddRouteComponents("v1", model1) + .AddRouteComponents("v1", model1, (services) => + { + services.AddODataQueryOptionsBindingExtension(new ExampleQueryOptionsBindingExtension()); + }) .AddRouteComponents("v2{data}", model2, services => services.AddSingleton()) .AddRouteComponents("v3", model3) .Conventions.Add(new MyConvention()) diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 2ea93aae3..147fae409 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -6299,6 +6299,22 @@ The to add the services to. The so that additional calls can be chained. + + + Adds a to a service collection. + + The to add the services to. + The to add to the service collection. + The so that additional calls can be chained. + + + + Adds multiple to a service collection. + + The to add the services to. + The to add to the service collection. + The so that additional calls can be chained. + OData UriFunctions helper. @@ -9474,6 +9490,42 @@ 'methodName(argTypeName1,argTypeName2,argTypeName3..)' + + + This interface allows to extend the method + and apply custom query features. + + + + + Apply a custom query to the given IQueryable. + + The original . + The object that is executing this method. + The settings to use in query composition. + The new after the query has been applied to. + + + + This class allows to use multiple extenion interfaces. + + + + + Initializes a new instance of the class + with a list of extension interfaces. + + A list of query extensions to apply. + + + + Apply multiple custom queries to the given IQueryable. + + The original . + The object that is executing this method. + The settings to use in query composition. + The new after the query has been applied to. + This enum defines how to handle null propagation in queryable support. diff --git a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs index 4481291fa..ba1700a86 100644 --- a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs @@ -5,11 +5,14 @@ // //------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; 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.Query.Extension; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Parser; using Microsoft.AspNetCore.OData.Routing.Template; @@ -105,5 +108,52 @@ internal static IServiceCollection AddODataCore(this IServiceCollection services return services; } + + /// + /// Adds a to a service collection. + /// + /// The to add the services to. + /// The to add to the service collection. + /// The so that additional calls can be chained. + public static IServiceCollection AddODataQueryOptionsBindingExtension(this IServiceCollection services, IODataQueryOptionsBindingExtension extension) + { + if (services == null) + { + throw Error.ArgumentNull(nameof(services)); + } + + if (services == null) + { + throw Error.ArgumentNull(nameof(extension)); + } + + services.AddSingleton(extension); + return services; + } + + /// + /// Adds multiple to a service collection. + /// + /// The to add the services to. + /// The to add to the service collection. + /// The so that additional calls can be chained. + public static IServiceCollection AddMultipleODataQueryOptionsBindingExtension(this IServiceCollection services, Action> extensions) + { + if (services == null) + { + throw Error.ArgumentNull(nameof(services)); + } + + if (extensions == null) + { + throw Error.ArgumentNull(nameof(extensions)); + } + + IList extensionResult = new List(); + extensions.Invoke(extensionResult); + services.AddSingleton(new MultipleODataQueryOptionsBindingExtension(extensionResult)); + + return services; + } } } diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index e40289656..d057c22d0 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -910,6 +910,11 @@ Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.FilterBinder.get -> Microsoft.AspNetCore.OData.Query.Expressions.IFilterBinder Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.OrderByBinder.get -> Microsoft.AspNetCore.OData.Query.Expressions.IOrderByBinder Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder.SelectExpandBinder(Microsoft.AspNetCore.OData.Query.Expressions.IFilterBinder filterBinder, Microsoft.AspNetCore.OData.Query.Expressions.IOrderByBinder orderByBinder) -> void +Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension +Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension.ApplyTo(System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) -> System.Linq.IQueryable +Microsoft.AspNetCore.OData.Query.Extension.MultipleODataQueryOptionsBindingExtension +Microsoft.AspNetCore.OData.Query.Extension.MultipleODataQueryOptionsBindingExtension.ApplyTo(System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) -> System.Linq.IQueryable +Microsoft.AspNetCore.OData.Query.Extension.MultipleODataQueryOptionsBindingExtension.MultipleODataQueryOptionsBindingExtension(System.Collections.Generic.IList extensions) -> void Microsoft.AspNetCore.OData.Query.FilterQueryOption Microsoft.AspNetCore.OData.Query.FilterQueryOption.ApplyTo(System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) -> System.Linq.IQueryable Microsoft.AspNetCore.OData.Query.FilterQueryOption.Compute.get -> Microsoft.AspNetCore.OData.Query.ComputeQueryOption @@ -1613,6 +1618,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.AddDataQueryOptionsBindingExtension(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension extension) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddMultipleDataQueryOptionsBindingExtension(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action> extensions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection 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 diff --git a/src/Microsoft.AspNetCore.OData/Query/Extension/IODataQueryOptionsBindingExtension.cs b/src/Microsoft.AspNetCore.OData/Query/Extension/IODataQueryOptionsBindingExtension.cs new file mode 100644 index 000000000..5c781090c --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Query/Extension/IODataQueryOptionsBindingExtension.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; + +namespace Microsoft.AspNetCore.OData.Query.Extension +{ + + /// + /// This interface allows to extend the method + /// and apply custom query features. + /// + public interface IODataQueryOptionsBindingExtension + { + + /// + /// Apply a custom query to the given IQueryable. + /// + /// The original . + /// The object that is executing this method. + /// The settings to use in query composition. + /// The new after the query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQueryOptions queryOptions, ODataQuerySettings querySettings); + + } +} diff --git a/src/Microsoft.AspNetCore.OData/Query/Extension/MultipleODataQueryOptionsBindingExtension.cs b/src/Microsoft.AspNetCore.OData/Query/Extension/MultipleODataQueryOptionsBindingExtension.cs new file mode 100644 index 000000000..53e53325f --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Query/Extension/MultipleODataQueryOptionsBindingExtension.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.OData.Query.Extension +{ + + /// + /// This class allows to use multiple extenion interfaces. + /// + public class MultipleODataQueryOptionsBindingExtension : IODataQueryOptionsBindingExtension + { + + private readonly IList _extensions; + + /// + /// Initializes a new instance of the class + /// with a list of extension interfaces. + /// + /// A list of query extensions to apply. + public MultipleODataQueryOptionsBindingExtension(IList extensions) + { + _extensions = extensions; + } + + /// + /// Apply multiple custom queries to the given IQueryable. + /// + /// The original . + /// The object that is executing this method. + /// The settings to use in query composition. + /// The new after the query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQueryOptions queryOptions, ODataQuerySettings querySettings) + { + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); + } + + if (queryOptions == null) + { + throw Error.ArgumentNull(nameof(queryOptions)); + } + + if (querySettings == null) + { + throw Error.ArgumentNull(nameof(querySettings)); + } + + IQueryable result = query; + foreach (var extension in _extensions) + { + result = extension.ApplyTo(query, queryOptions, querySettings); + } + return result; + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs index 86dcb3e05..e92add89d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Query.Container; +using Microsoft.AspNetCore.OData.Query.Extension; using Microsoft.AspNetCore.OData.Query.Validator; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; @@ -455,6 +456,8 @@ public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySett result = Top.ApplyTo(result, querySettings); } + result = ApplyCustom(result, querySettings); + result = ApplyPaging(result, querySettings); return result; @@ -495,6 +498,22 @@ internal IQueryable ApplyPaging(IQueryable result, ODataQuerySettings querySetti return result; } + internal IQueryable ApplyCustom(IQueryable query, ODataQuerySettings querySettings) + { + if(Context.RequestContainer != null) + { + IODataQueryOptionsBindingExtension extension = Context.RequestContainer + .GetService(); + + if(extension != null) + { + return extension.ApplyTo(query, this, querySettings); + } + } + + return query; + } + /// /// Generates the Stable OrderBy query option based on the existing OrderBy and other query options. /// 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 87b1c80ee..aa7221b1f 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 @@ -67,6 +67,11 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { ExtensionAttribute(), ] public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMultipleODataQueryOptionsBindingExtension (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`1[[System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension]]]] extensions) + [ ExtensionAttribute(), ] @@ -76,6 +81,11 @@ public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions ExtensionAttribute(), ] public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryOptionsBindingExtension (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension extension) } public sealed class Microsoft.AspNetCore.OData.ODataUriFunctions { @@ -2753,6 +2763,16 @@ public class Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder : M public virtual System.Linq.Expressions.Expression CreateTypeNameExpression (System.Linq.Expressions.Expression source, Microsoft.OData.Edm.IEdmStructuredType elementType, Microsoft.OData.Edm.IEdmModel model) } +public interface Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension { + System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) +} + +public class Microsoft.AspNetCore.OData.Query.Extension.MultipleODataQueryOptionsBindingExtension : IODataQueryOptionsBindingExtension { + public MultipleODataQueryOptionsBindingExtension (System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension]] extensions) + + public virtual System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) +} + public class Microsoft.AspNetCore.OData.Query.Validator.CountQueryValidator { public CountQueryValidator () 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 87b1c80ee..aa7221b1f 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 @@ -67,6 +67,11 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { ExtensionAttribute(), ] public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMultipleODataQueryOptionsBindingExtension (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`1[[System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension]]]] extensions) + [ ExtensionAttribute(), ] @@ -76,6 +81,11 @@ public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions ExtensionAttribute(), ] public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryFilter (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddODataQueryOptionsBindingExtension (Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension extension) } public sealed class Microsoft.AspNetCore.OData.ODataUriFunctions { @@ -2753,6 +2763,16 @@ public class Microsoft.AspNetCore.OData.Query.Expressions.SelectExpandBinder : M public virtual System.Linq.Expressions.Expression CreateTypeNameExpression (System.Linq.Expressions.Expression source, Microsoft.OData.Edm.IEdmStructuredType elementType, Microsoft.OData.Edm.IEdmModel model) } +public interface Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension { + System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) +} + +public class Microsoft.AspNetCore.OData.Query.Extension.MultipleODataQueryOptionsBindingExtension : IODataQueryOptionsBindingExtension { + public MultipleODataQueryOptionsBindingExtension (System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Query.Extension.IODataQueryOptionsBindingExtension]] extensions) + + public virtual System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQueryOptions queryOptions, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) +} + public class Microsoft.AspNetCore.OData.Query.Validator.CountQueryValidator { public CountQueryValidator () diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/QueryOptionsBindingExtensionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/QueryOptionsBindingExtensionTests.cs new file mode 100644 index 000000000..e6d5c467f --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/QueryOptionsBindingExtensionTests.cs @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Query.Extension; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.AspNetCore.OData.Tests.Extensions; +using Microsoft.AspNetCore.OData.Tests.Models; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Query +{ + public class QueryOptionsBindingExtensionTests + { + + [Fact] + public void ODataQueryOptions_ApplyCustom_RequestContainerIsNull() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); + var context = new ODataQueryContext(CreateModel(), typeof(Customer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + context.RequestContainer = null; + ODataQuerySettings querySettings = context.GetODataQuerySettings(); + var customers = CreateTestQueryable(); + + // Act + var result = queryOptions.ApplyCustom(customers, querySettings); + + // Arrange + Assert.Equal(customers, result); + } + + [Fact] + public void ODataQueryOptions_ApplyCustom_NoExtensionHandlerProvided() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); + var context = new ODataQueryContext(CreateModel(), typeof(Customer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + ODataQuerySettings querySettings = context.GetODataQuerySettings(); + var customers = CreateTestQueryable(); + + // Act + var result = queryOptions.ApplyCustom(customers, querySettings); + + // Arrange + Assert.Equal(customers, result); + } + + [Fact] + public void ODataQueryOptions_ApplyCustom_WithExtensionHandlerProvided() + { + // Arrange + IServiceProvider serviceProvider = new MockServiceProvider((builder) => + { + builder.AddService(ServiceLifetime.Singleton + ,typeof(IODataQueryOptionsBindingExtension) + ,typeof(MockODataQueryOptionsBindingExtension)); + }); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); + var context = new ODataQueryContext(CreateModel(), typeof(Customer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + context.RequestContainer = serviceProvider; + ODataQuerySettings querySettings = context.GetODataQuerySettings(); + var customers = CreateTestQueryable(); + + var expected = ApplyTestExpressions(customers,querySettings); + + // Act + var result = queryOptions.ApplyCustom(customers, querySettings); + + // Arrange + Assert.Equal(expected, result); + } + + [Fact] + public void MultipleODataQueryOptionsBindingExtension_ApplyTo() + { + // Arrange + MultipleODataQueryOptionsBindingExtension extensions = new MultipleODataQueryOptionsBindingExtension( + new List{ + new MockODataQueryOptionsBindingExtension(), + new MockODataQueryOptionsBindingExtension(), + }); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); + var context = new ODataQueryContext(CreateModel(), typeof(Customer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + ODataQuerySettings querySettings = context.GetODataQuerySettings(); + + var customers = CreateTestQueryable(); + + var expected = ApplyTestExpressions(ApplyTestExpressions(customers, querySettings), querySettings); + + // Act + var result = extensions.ApplyTo(customers, queryOptions, querySettings); + + // Arrange + Assert.Equal(expected, result); + } + + private static IEdmModel CreateModel() + { + return new ODataModelBuilder() + .Add_Customer_EntityType_With_Address() + .Add_Customers_EntitySet().GetEdmModel(); + } + + private static IQueryable CreateTestQueryable() + { + return (new List{ + new Customer { Id = 1, Address = new Address { City = "A" } }, + new Customer { Id = 2, Address = new Address { City = "B" } }, + new Customer { Id = 3, Address = new Address { City = "C" } } + }).AsQueryable(); + } + + //Used skip expression for tests, because it has a simple usage + private static IQueryable ApplyTestExpressions(IQueryable queryable, ODataQuerySettings querySettings) + { + return ExpressionHelpers.Skip(queryable, 10, queryable.ElementType, querySettings.EnableConstantParameterization); + } + + private class MockODataQueryOptionsBindingExtension : IODataQueryOptionsBindingExtension + { + public IQueryable ApplyTo(IQueryable query, ODataQueryOptions queryOptions, ODataQuerySettings querySettings) + { + return ApplyTestExpressions(query, querySettings); + } + } + + } +}