diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs index 83beb1bc1..91caeeb62 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs @@ -271,12 +271,6 @@ public static IServiceProvider GetRouteServices(this HttpRequest request) return requestContainer; } - // if the prefixName == null, it's a non-model scenario - if (request.ODataFeature().RoutePrefix == null) - { - return null; - } - // HTTP routes will not have chance to call CreateRequestContainer. We have to call it. return request.CreateRouteServices(request.ODataFeature().RoutePrefix); } @@ -300,6 +294,12 @@ public static IServiceProvider CreateRouteServices(this HttpRequest request, str } IServiceScope requestScope = request.CreateRequestScope(routePrefix); + if (requestScope == null) + { + // non-model scenario with dependency injection non enabled + return null; + } + IServiceProvider requestContainer = requestScope.ServiceProvider; request.ODataFeature().RequestScope = requestScope; @@ -341,15 +341,17 @@ private static IServiceScope CreateRequestScope(this HttpRequest request, string { ODataOptions options = request.ODataOptions(); - IServiceProvider rootContainer = options.GetRouteServices(routePrefix); - IServiceScope scope = rootContainer.GetRequiredService().CreateScope(); - - // Bind scoping request into the OData container. - if (!string.IsNullOrEmpty(routePrefix)) + IServiceProvider rootContainer = options?.GetRouteServices(routePrefix); + if (rootContainer == null) { - scope.ServiceProvider.GetRequiredService().HttpRequest = request; + return null; } + IServiceScope scope = rootContainer.GetRequiredService().CreateScope(); + + // Bind scoping request into the OData container. + scope.ServiceProvider.GetRequiredService().HttpRequest = request; + return scope; } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index e900babf5..f6ad8baf4 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -6160,6 +6160,21 @@ DO NOT modify this dictionary yourself. Instead, use the 'AddRouteComponents()` methods for registering model instances. + + + Configures service collection for non-EDM scenario + + The sub service configuration action. + The current instance to enable fluent configuration. + + + + Configures service collection for non-EDM scenario + + The OData version to be used. + The sub service configuration action. + The current instance to enable fluent configuration. + Adds an to the default route. @@ -6285,8 +6300,8 @@ Build the container. The Edm model. - The setup config. The OData version config. + The setup config. The built service provider. diff --git a/src/Microsoft.AspNetCore.OData/ODataOptions.cs b/src/Microsoft.AspNetCore.OData/ODataOptions.cs index 09edefefa..e0efdf8dd 100644 --- a/src/Microsoft.AspNetCore.OData/ODataOptions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataOptions.cs @@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.OData public class ODataOptions { #region Settings + private IServiceProvider _serviceProvider; + /// /// Gets or sets the to use while parsing, specifically /// whether to recognize keys as segments or not. @@ -61,7 +63,7 @@ public class ODataOptions /// /// Gets the instance responsible for configuring the route template. /// - public ODataRouteOptions RouteOptions { get; } = new ODataRouteOptions(); + public ODataRouteOptions RouteOptions { get; } = new ODataRouteOptions(); #endregion @@ -73,6 +75,28 @@ public class ODataOptions /// DO NOT modify this dictionary yourself. Instead, use the 'AddRouteComponents()` methods for registering model instances. public IDictionary RouteComponents { get; } = new Dictionary(); + /// + /// Configures service collection for non-EDM scenario + /// + /// The sub service configuration action. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(Action configureServices) + { + return AddRouteComponents(ODataVersion.V4, configureServices); + } + + /// + /// Configures service collection for non-EDM scenario + /// + /// The OData version to be used. + /// The sub service configuration action. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(ODataVersion version, Action configureServices) + { + _serviceProvider = BuildRouteContainer(null, version, configureServices); + return this; + } + /// /// Adds an to the default route. /// @@ -171,7 +195,7 @@ public IServiceProvider GetRouteServices(string routePrefix) { if (routePrefix == null) { - return null; + return _serviceProvider; } string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix); @@ -298,13 +322,11 @@ public ODataOptions SetMaxTop(int? maxTopValue) /// Build the container. /// /// The Edm model. - /// The setup config. /// The OData version config. + /// The setup config. /// The built service provider. private IServiceProvider BuildRouteContainer(IEdmModel model, ODataVersion version, Action setupAction) { - Contract.Assert(model != null); - ServiceCollection services = new ServiceCollection(); DefaultContainerBuilder builder = new DefaultContainerBuilder(); @@ -325,10 +347,13 @@ private IServiceProvider BuildRouteContainer(IEdmModel model, ODataVersion versi EnableNoDollarQueryOptions = EnableNoDollarQueryOptions // retrieve it from global setting }); - // Inject the Edm model. - // From Current ODL implement, such injection only be used in reader and writer if the input - // model is null. - builder.Services.AddSingleton(sp => model); + if (model != null) + { + // Inject the Edm model. + // From Current ODL implement, such injection only be used in reader and writer if the input + // model is null. + builder.Services.AddSingleton(sp => model); + } // Inject the customized services. setupAction?.Invoke(builder.Services); diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index ae7f1bcaa..a0b0d361e 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -648,6 +648,8 @@ Microsoft.AspNetCore.OData.ODataMvcOptionsSetup Microsoft.AspNetCore.OData.ODataMvcOptionsSetup.Configure(Microsoft.AspNetCore.Mvc.MvcOptions options) -> void Microsoft.AspNetCore.OData.ODataMvcOptionsSetup.ODataMvcOptionsSetup() -> void Microsoft.AspNetCore.OData.ODataOptions +Microsoft.AspNetCore.OData.ODataOptions.AddRouteComponents(System.Action configureServices) -> Microsoft.AspNetCore.OData.ODataOptions +Microsoft.AspNetCore.OData.ODataOptions.AddRouteComponents(Microsoft.OData.ODataVersion version, System.Action configureServices) -> Microsoft.AspNetCore.OData.ODataOptions Microsoft.AspNetCore.OData.ODataOptions.AddRouteComponents(Microsoft.OData.Edm.IEdmModel model) -> Microsoft.AspNetCore.OData.ODataOptions Microsoft.AspNetCore.OData.ODataOptions.AddRouteComponents(Microsoft.OData.Edm.IEdmModel model, Microsoft.AspNetCore.OData.Batch.ODataBatchHandler batchHandler) -> Microsoft.AspNetCore.OData.ODataOptions Microsoft.AspNetCore.OData.ODataOptions.AddRouteComponents(string routePrefix, Microsoft.OData.Edm.IEdmModel model) -> Microsoft.AspNetCore.OData.ODataOptions diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj b/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj index 6c795b7bd..234789ddd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj @@ -1,6 +1,8 @@  + netcoreapp3.1;net5.0 + $(TargetFrameworks);net6.0 netcoreapp3.1;net6.0 Microsoft.AspNetCore.OData.E2E.Tests Microsoft.AspNetCore.OData.E2E.Tests diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/CustomersController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/CustomersController.cs new file mode 100644 index 000000000..589e04bb0 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/CustomersController.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Query; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.NonEdm +{ + [ApiController] + [Route("api/[controller]")] + public class CustomersController : ControllerBase + { + [HttpGet] + public IActionResult Get(ODataQueryOptions options) + { + return Ok(options.ApplyTo(NonEdmDbContext.GetCustomers().AsQueryable())); + } + + [HttpGet("WithEnableQueryAttribute")] + [EnableQuery] + public IActionResult GetWithEnableQueryAttribute() + { + return Ok(NonEdmDbContext.GetCustomers()); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDataModel.cs new file mode 100644 index 000000000..0e23f85e6 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDataModel.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.OData.E2E.Tests.NonEdm +{ + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public Gender Gender { get; set; } + } + + public enum Gender + { + Male = 1, + Female = 2 + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDbContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDbContext.cs new file mode 100644 index 000000000..b49537984 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmDbContext.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// +// 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.E2E.Tests.NonEdm +{ + public class NonEdmDbContext + { + private static IList _customers; + + public static IList GetCustomers() + { + if (_customers == null) + { + Generate(); + } + return _customers; + } + + private static void Generate() + { + _customers = Enumerable.Range(1, 10).Select(e => + new Customer + { + Id = e, + Name = "Customer #" + e, + Gender = e%2 == 0 ? Gender.Female : Gender.Male, + }).ToList(); + } + } + +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmTests.cs new file mode 100644 index 000000000..864a30ec2 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NonEdm/NonEdmTests.cs @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.E2E.Tests.Extensions; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.UriParser; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.NonEdm +{ + public class NonEdmTests : WebApiTestBase + { + public NonEdmTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController)); + services.AddControllers().AddOData(opt => + { + opt.EnableQueryFeatures(); + opt.AddRouteComponents(services => + { + services.AddSingleton(sp => new StringAsEnumResolver() { EnableCaseInsensitive = true }); + }); + }); + } + + [Fact] + public async Task NonEdmFilterByEnumString() + { + Assert.Equal(5, (await GetResponse("$filter=Gender eq 'MaLe'")).Length); + } + + [Fact] + public async Task NonEdmFilterByEnumStringWithEnableQueryAttribute() + { + Assert.Equal(5, (await GetResponse("$filter=Gender eq 'MaLe'", "WithEnableQueryAttribute")).Length); + } + + [Fact] + public async Task NonEdmSumFilteredByEnumString() + { + var response = await GetResponse("$apply=filter(Gender eq 'female')/aggregate(Id with sum as Sum)"); + Assert.Equal(30, response.Single()["Sum"].Value()); + } + + [Fact] + public async Task NonEdmSelectTopFilteredByEnumString() + { + var response = await GetResponse("$filter(Gender eq 'female')&$orderby=Id desc&$select=Name&$top=1&$skip=1"); + Assert.Equal("Customer #9", response.Single().Name); + } + + private async Task GetResponse(string queryOptions, string method = null) + { + using var response = await CreateClient().SendAsync( + new HttpRequestMessage(HttpMethod.Get, $"api/Customers/{method ?? string.Empty}?{queryOptions}")); + return await response.Content.ReadAsObject(); + } + } +} 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 41ec65407..0620d35ba 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 @@ -117,6 +117,7 @@ public class Microsoft.AspNetCore.OData.ODataOptions { public Microsoft.AspNetCore.OData.ODataOptions AddRouteComponents (string routePrefix, Microsoft.OData.Edm.IEdmModel model) public Microsoft.AspNetCore.OData.ODataOptions AddRouteComponents (string routePrefix, Microsoft.OData.Edm.IEdmModel model, Microsoft.AspNetCore.OData.Batch.ODataBatchHandler batchHandler) public Microsoft.AspNetCore.OData.ODataOptions AddRouteComponents (string routePrefix, Microsoft.OData.Edm.IEdmModel model, System.Action`1[[Microsoft.Extensions.DependencyInjection.IServiceCollection]] configureServices) + public Microsoft.AspNetCore.OData.ODataOptions ConfigureServiceCollection (System.Action`1[[Microsoft.Extensions.DependencyInjection.IServiceCollection]] configureServices) public Microsoft.AspNetCore.OData.ODataOptions Count () public Microsoft.AspNetCore.OData.ODataOptions EnableQueryFeatures (params System.Nullable`1[[System.Int32]] maxTopValue) public Microsoft.AspNetCore.OData.ODataOptions Expand ()