diff --git a/src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs b/src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs index 252345fe112b..8b18b7928d7f 100644 --- a/src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs +++ b/src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs @@ -5,6 +5,7 @@ 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 Func InvalidModelStateResponseFactory /// When enabled, the following sources are inferred: /// Parameters that appear as route values, are assumed to be bound from the path (). /// Parameters of type and are assumed to be bound from form. + /// Parameters that are complex () and are registered in the DI Container () are assumed to be bound from the services , unless this + /// option is explicitly disabled . /// Parameters that are complex () are assumed to be bound from the body (). /// All other parameters are assumed to be bound from the query. /// /// public bool SuppressInferBindingSourcesForParameters { get; set; } + /// + /// Gets or sets a value that determines if parameters are inferred to be from services. + /// This property is only applicable when is . + /// + public bool DisableImplicitFromServicesParameters { get; set; } + /// /// Gets or sets a value that determines if an multipart/form-data consumes action constraint is added to parameters /// that are bound from form data. diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs index 496187323817..1872faebf0c6 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs +++ b/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 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 @@ public ApiBehaviorApplicationModelProvider( IOptions apiBehaviorOptions, IModelMetadataProvider modelMetadataProvider, IClientErrorFactory clientErrorFactory, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider) { var options = apiBehaviorOptions.Value; @@ -47,7 +49,11 @@ public ApiBehaviorApplicationModelProvider( if (!options.SuppressInferBindingSourcesForParameters) { - ActionModelConventions.Add(new InferParameterBindingInfoConvention(modelMetadataProvider)); + var serviceProviderIsService = serviceProvider.GetService(); + var convention = options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ? + new InferParameterBindingInfoConvention(modelMetadataProvider) : + new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService); + ActionModelConventions.Add(convention); } } diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/InferParameterBindingInfoConvention.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/InferParameterBindingInfoConvention.cs index fa8c9fd2245b..11258a03b138 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/InferParameterBindingInfoConvention.cs +++ b/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 inferences. The rules are: /// /// A previously specified is never overwritten. -/// A complex type parameter () is assigned . +/// A complex type parameter (), registered in the DI container, is assigned . +/// A complex type parameter (), not registered in the DI container, is assigned . /// Parameter with a name that appears as a route value in ANY route template is assigned . /// All other parameters are . /// @@ -23,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels; public class InferParameterBindingInfoConvention : IActionModelConvention { private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly IServiceProviderIsService? _serviceProviderIsService; /// /// Initializes a new instance of . @@ -34,6 +37,21 @@ public InferParameterBindingInfoConvention( _modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider)); } + /// + /// Initializes a new instance of . + /// + /// The model metadata provider. + /// The service to determine if the a type is available from the . + public InferParameterBindingInfoConvention( + IModelMetadataProvider modelMetadataProvider, + IServiceProviderIsService serviceProviderIsService) + : this(modelMetadataProvider) + { + _serviceProviderIsService = serviceProviderIsService ?? throw new ArgumentNullException(nameof(serviceProviderIsService)); + } + + internal bool IsInferForServiceParametersEnabled => _serviceProviderIsService != null; + /// /// Called to determine whether the action should apply. /// @@ -95,6 +113,11 @@ internal BindingSource InferBindingSourceForParameter(ParameterModel parameter) { if (IsComplexTypeParameter(parameter)) { + if (_serviceProviderIsService?.IsService(parameter.ParameterType) is true) + { + return BindingSource.Services; + } + return BindingSource.Body; } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index d326535ca4d1..2c04e5427635 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/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 diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs index 34d9196d0c32..224198672ee1 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs +++ b/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 void Constructor_DoesNotAddInferParameterBindingInfoConvention_IfSuppress Assert.Empty(provider.ActionModelConventions.OfType()); } + [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 @@ private static ApiBehaviorApplicationModelProvider GetProvider( optionsAccessor, new EmptyModelMetadataProvider(), Mock.Of(), - loggerFactory); + loggerFactory, + Mock.Of()); } private class TestApiController : ControllerBase diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs index 70832d7ec47e..de561eed5c29 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs +++ b/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 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 @@ public void InferBindingSourceForParameter_ReturnsBodyForCollectionOfComplexType 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(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 @@ public void PreservesBindingSourceInference_ForParameterWithRequestPredicateAndP } 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(s => s.IsService(It.IsAny()) == false); + return new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService); } private static ApplicationModelProviderContext GetContext( @@ -871,6 +893,8 @@ private class ParameterBindingController public IActionResult CollectionOfSimpleTypes(IList parameter) => null; public IActionResult CollectionOfComplexTypes(IList parameter) => null; + + public IActionResult ServiceParameter(IApplicationModelProvider parameter) => null; } [ApiController] diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs index db20a20dcdd4..98c6cb955587 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -145,6 +145,21 @@ private async Task ActionsWithApiBehaviorInferFromBodyParameters(string action) 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(await response.Content.ReadAsStringAsync()); + Assert.NotNull(result); + Assert.Equal(id, result.ContactId); + } + [Fact] public async Task ActionsWithApiBehavior_InferQueryAndRouteParameters() { diff --git a/src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs b/src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs index 8979663f44d0..18cdc433222c 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs @@ -83,6 +83,10 @@ public ActionResult ActionWithInferredModelBinderTypeWithExplicitModelNa return foo; } + [HttpGet("[action]/{id}")] + public ActionResult ActionWithInferredFromServicesParameter(int id, ContactsRepository repository) + => repository.GetContact(id) ?? new Contact() { ContactId = id }; + [HttpGet("[action]")] public ActionResult ActionReturningStatusCodeResult() {