Просмотр исходного кода

Adding support to infer FromServices parameters (#39926)

* Inferring FromServices

* Adding unit tests

* Removing API change

* API Review feedback

* Remove empty line

* nit: feeback

* Clean up usings
Bruno Oliveira 4 лет назад
Родитель
Сommit
44d89fa215

+ 9 - 0
src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs

@@ -5,6 +5,7 @@ using System.Collections;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.DependencyInjection;
 
 namespace Microsoft.AspNetCore.Mvc;
 
@@ -39,12 +40,20 @@ public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
     /// When enabled, the following sources are inferred:
     /// Parameters that appear as route values, are assumed to be bound from the path (<see cref="BindingSource.Path"/>).
     /// Parameters of type <see cref="IFormFile"/> and <see cref="IFormFileCollection"/> are assumed to be bound from form.
+    /// Parameters that are complex (<see cref="ModelMetadata.IsComplexType"/>) and are registered in the DI Container (<see cref="IServiceCollection"/>) are assumed to be bound from the services <see cref="BindingSource.Services"/>, unless this
+    /// option is explicitly disabled <see cref="DisableImplicitFromServicesParameters"/>.
     /// Parameters that are complex (<see cref="ModelMetadata.IsComplexType"/>) are assumed to be bound from the body (<see cref="BindingSource.Body"/>).
     /// All other parameters are assumed to be bound from the query.
     /// </para>
     /// </summary>
     public bool SuppressInferBindingSourcesForParameters { get; set; }
 
+    /// <summary>
+    /// Gets or sets a value that determines if parameters are inferred to be from services.
+    /// This property is only applicable when <see cref="SuppressInferBindingSourcesForParameters" /> is <see langword="false" />.
+    /// </summary>
+    public bool DisableImplicitFromServicesParameters { get; set; }
+
     /// <summary>
     /// Gets or sets a value that determines if an <c>multipart/form-data</c> consumes action constraint is added to parameters
     /// that are bound from form data.

+ 9 - 3
src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs

@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Linq;
@@ -6,6 +6,7 @@ using System.Reflection;
 using Microsoft.AspNetCore.Mvc.Core;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 
@@ -17,7 +18,8 @@ internal class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
         IOptions<ApiBehaviorOptions> apiBehaviorOptions,
         IModelMetadataProvider modelMetadataProvider,
         IClientErrorFactory clientErrorFactory,
-        ILoggerFactory loggerFactory)
+        ILoggerFactory loggerFactory,
+        IServiceProvider serviceProvider)
     {
         var options = apiBehaviorOptions.Value;
 
@@ -47,7 +49,11 @@ internal class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
 
         if (!options.SuppressInferBindingSourcesForParameters)
         {
-            ActionModelConventions.Add(new InferParameterBindingInfoConvention(modelMetadataProvider));
+            var serviceProviderIsService = serviceProvider.GetService<IServiceProviderIsService>();
+            var convention = options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ?
+                new InferParameterBindingInfoConvention(modelMetadataProvider) :
+                new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
+            ActionModelConventions.Add(convention);
         }
     }
 

+ 24 - 1
src/Mvc/Mvc.Core/src/ApplicationModels/InferParameterBindingInfoConvention.cs

@@ -4,6 +4,7 @@
 using System.Linq;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.DependencyInjection;
 using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
 
 namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
@@ -15,7 +16,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
 /// The goal of this convention is to make intuitive and easy to document <see cref="BindingSource"/> inferences. The rules are:
 /// <list type="number">
 /// <item>A previously specified <see cref="BindingInfo.BindingSource" /> is never overwritten.</item>
-/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>) is assigned <see cref="BindingSource.Body"/>.</item>
+/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), registered in the DI container, is assigned <see cref="BindingSource.Services"/>.</item>
+/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), not registered in the DI container, is assigned <see cref="BindingSource.Body"/>.</item>
 /// <item>Parameter with a name that appears as a route value in ANY route template is assigned <see cref="BindingSource.Path"/>.</item>
 /// <item>All other parameters are <see cref="BindingSource.Query"/>.</item>
 /// </list>
@@ -23,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
 public class InferParameterBindingInfoConvention : IActionModelConvention
 {
     private readonly IModelMetadataProvider _modelMetadataProvider;
+    private readonly IServiceProviderIsService? _serviceProviderIsService;
 
     /// <summary>
     /// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>.
@@ -34,6 +37,21 @@ public class InferParameterBindingInfoConvention : IActionModelConvention
         _modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
     }
 
+    /// <summary>
+    /// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>.
+    /// </summary>
+    /// <param name="modelMetadataProvider">The model metadata provider.</param>
+    /// <param name="serviceProviderIsService">The service to determine if the a type is available from the <see cref="IServiceProvider"/>.</param>
+    public InferParameterBindingInfoConvention(
+        IModelMetadataProvider modelMetadataProvider,
+        IServiceProviderIsService serviceProviderIsService)
+        : this(modelMetadataProvider)
+    {
+        _serviceProviderIsService = serviceProviderIsService ?? throw new ArgumentNullException(nameof(serviceProviderIsService));
+    }
+
+    internal bool IsInferForServiceParametersEnabled => _serviceProviderIsService != null;
+
     /// <summary>
     /// Called to determine whether the action should apply.
     /// </summary>
@@ -95,6 +113,11 @@ public class InferParameterBindingInfoConvention : IActionModelConvention
     {
         if (IsComplexTypeParameter(parameter))
         {
+            if (_serviceProviderIsService?.IsService(parameter.ParameterType) is true)
+            {
+                return BindingSource.Services;
+            }
+
             return BindingSource.Body;
         }
 

+ 3 - 0
src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt

@@ -1,4 +1,7 @@
 #nullable enable
+Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.DisableImplicitFromServicesParameters.get -> bool
+Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.DisableImplicitFromServicesParameters.set -> void
+Microsoft.AspNetCore.Mvc.ApplicationModels.InferParameterBindingInfoConvention.InferParameterBindingInfoConvention(Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider! modelMetadataProvider, Microsoft.Extensions.DependencyInjection.IServiceProviderIsService! serviceProviderIsService) -> void
 Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider
 Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
 Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void

+ 14 - 2
src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs

@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Reflection;
@@ -138,6 +138,17 @@ public class ApiBehaviorApplicationModelProviderTest
         Assert.Empty(provider.ActionModelConventions.OfType<InferParameterBindingInfoConvention>());
     }
 
+    [Fact]
+    public void Constructor_DoesNotInferServicesParameterBindingInfoConvention_IfSuppressInferBindingSourcesForParametersIsSet()
+    {
+        // Arrange
+        var provider = GetProvider(new ApiBehaviorOptions { DisableImplicitFromServicesParameters = true });
+
+        // Act & Assert
+        var convention = (InferParameterBindingInfoConvention)Assert.Single(provider.ActionModelConventions, c => c is InferParameterBindingInfoConvention);
+        Assert.False(convention.IsInferForServiceParametersEnabled);
+    }
+
     [Fact]
     public void Constructor_DoesNotSpecifyDefaultErrorType_IfSuppressMapClientErrorsIsSet()
     {
@@ -163,7 +174,8 @@ public class ApiBehaviorApplicationModelProviderTest
             optionsAccessor,
             new EmptyModelMetadataProvider(),
             Mock.Of<IClientErrorFactory>(),
-            loggerFactory);
+            loggerFactory,
+            Mock.Of<IServiceProvider>());
     }
 
     private class TestApiController : ControllerBase

+ 27 - 3
src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs

@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel;
@@ -6,7 +6,9 @@ using System.Reflection;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Options;
+using Moq;
 
 namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
 
@@ -477,6 +479,24 @@ Environment.NewLine + "int b";
         Assert.Same(BindingSource.Body, result);
     }
 
+    [Fact]
+    public void InferBindingSourceForParameter_ReturnsServicesForComplexTypesRegisteredInDI()
+    {
+        // Arrange
+        var actionName = nameof(ParameterBindingController.ServiceParameter);
+        var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
+        // Using any built-in type defined in the Test action
+        var serviceProvider = Mock.Of<IServiceProviderIsService>(s => s.IsService(typeof(IApplicationModelProvider)) == true);
+        var convention = GetConvention(serviceProviderIsService: serviceProvider);
+
+        // Act
+        var result = convention.InferBindingSourceForParameter(parameter);
+
+        // Assert
+        Assert.True(convention.IsInferForServiceParametersEnabled);
+        Assert.Same(BindingSource.Services, result);
+    }
+
     [Fact]
     public void PreservesBindingSourceInference_ForFromQueryParameter_WithDefaultName()
     {
@@ -732,10 +752,12 @@ Environment.NewLine + "int b";
     }
 
     private static InferParameterBindingInfoConvention GetConvention(
-        IModelMetadataProvider modelMetadataProvider = null)
+        IModelMetadataProvider modelMetadataProvider = null,
+        IServiceProviderIsService serviceProviderIsService = null)
     {
         modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
-        return new InferParameterBindingInfoConvention(modelMetadataProvider);
+        serviceProviderIsService = serviceProviderIsService ?? Mock.Of<IServiceProviderIsService>(s => s.IsService(It.IsAny<Type>()) == false);
+        return new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
     }
 
     private static ApplicationModelProviderContext GetContext(
@@ -871,6 +893,8 @@ Environment.NewLine + "int b";
         public IActionResult CollectionOfSimpleTypes(IList<int> parameter) => null;
 
         public IActionResult CollectionOfComplexTypes(IList<TestModel> parameter) => null;
+
+        public IActionResult ServiceParameter(IApplicationModelProvider parameter) => null;
     }
 
     [ApiController]

+ 15 - 0
src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs

@@ -145,6 +145,21 @@ public abstract class ApiBehaviorTestBase<TStartup> : IClassFixture<MvcTestFixtu
         Assert.Equal(input.Name, result.Name);
     }
 
+    [Fact]
+    public async Task ActionsWithApiBehavior_InferFromServicesParameters()
+    {
+        // Arrange
+        var id = 1;
+        var url = $"/contact/ActionWithInferredFromServicesParameter/{id}";
+        var response = await Client.GetAsync(url);
+
+        // Assert
+        await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+        var result = JsonConvert.DeserializeObject<Contact>(await response.Content.ReadAsStringAsync());
+        Assert.NotNull(result);
+        Assert.Equal(id, result.ContactId);
+    }
+
     [Fact]
     public async Task ActionsWithApiBehavior_InferQueryAndRouteParameters()
     {

+ 4 - 0
src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs

@@ -83,6 +83,10 @@ public class ContactApiController : Controller
         return foo;
     }
 
+    [HttpGet("[action]/{id}")]
+    public ActionResult<Contact> ActionWithInferredFromServicesParameter(int id, ContactsRepository repository)
+        => repository.GetContact(id) ?? new Contact() { ContactId = id };
+
     [HttpGet("[action]")]
     public ActionResult<int> ActionReturningStatusCodeResult()
     {