Skip to content

Commit 44d89fa

Browse files
authored
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
1 parent c2cf4e9 commit 44d89fa

File tree

8 files changed

+105
-9
lines changed

8 files changed

+105
-9
lines changed

src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Mvc.Infrastructure;
77
using Microsoft.AspNetCore.Mvc.ModelBinding;
8+
using Microsoft.Extensions.DependencyInjection;
89

910
namespace Microsoft.AspNetCore.Mvc;
1011

@@ -39,12 +40,20 @@ public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory
3940
/// When enabled, the following sources are inferred:
4041
/// Parameters that appear as route values, are assumed to be bound from the path (<see cref="BindingSource.Path"/>).
4142
/// Parameters of type <see cref="IFormFile"/> and <see cref="IFormFileCollection"/> are assumed to be bound from form.
43+
/// 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
44+
/// option is explicitly disabled <see cref="DisableImplicitFromServicesParameters"/>.
4245
/// Parameters that are complex (<see cref="ModelMetadata.IsComplexType"/>) are assumed to be bound from the body (<see cref="BindingSource.Body"/>).
4346
/// All other parameters are assumed to be bound from the query.
4447
/// </para>
4548
/// </summary>
4649
public bool SuppressInferBindingSourcesForParameters { get; set; }
4750

51+
/// <summary>
52+
/// Gets or sets a value that determines if parameters are inferred to be from services.
53+
/// This property is only applicable when <see cref="SuppressInferBindingSourcesForParameters" /> is <see langword="false" />.
54+
/// </summary>
55+
public bool DisableImplicitFromServicesParameters { get; set; }
56+
4857
/// <summary>
4958
/// Gets or sets a value that determines if an <c>multipart/form-data</c> consumes action constraint is added to parameters
5059
/// that are bound from form data.

src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Linq;
55
using System.Reflection;
66
using Microsoft.AspNetCore.Mvc.Core;
77
using Microsoft.AspNetCore.Mvc.Infrastructure;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
9+
using Microsoft.Extensions.DependencyInjection;
910
using Microsoft.Extensions.Logging;
1011
using Microsoft.Extensions.Options;
1112

@@ -17,7 +18,8 @@ public ApiBehaviorApplicationModelProvider(
1718
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
1819
IModelMetadataProvider modelMetadataProvider,
1920
IClientErrorFactory clientErrorFactory,
20-
ILoggerFactory loggerFactory)
21+
ILoggerFactory loggerFactory,
22+
IServiceProvider serviceProvider)
2123
{
2224
var options = apiBehaviorOptions.Value;
2325

@@ -47,7 +49,11 @@ public ApiBehaviorApplicationModelProvider(
4749

4850
if (!options.SuppressInferBindingSourcesForParameters)
4951
{
50-
ActionModelConventions.Add(new InferParameterBindingInfoConvention(modelMetadataProvider));
52+
var serviceProviderIsService = serviceProvider.GetService<IServiceProviderIsService>();
53+
var convention = options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ?
54+
new InferParameterBindingInfoConvention(modelMetadataProvider) :
55+
new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
56+
ActionModelConventions.Add(convention);
5157
}
5258
}
5359

src/Mvc/Mvc.Core/src/ApplicationModels/InferParameterBindingInfoConvention.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using Microsoft.AspNetCore.Mvc.ModelBinding;
66
using Microsoft.AspNetCore.Routing.Template;
7+
using Microsoft.Extensions.DependencyInjection;
78
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
89

910
namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
@@ -15,14 +16,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
1516
/// The goal of this convention is to make intuitive and easy to document <see cref="BindingSource"/> inferences. The rules are:
1617
/// <list type="number">
1718
/// <item>A previously specified <see cref="BindingInfo.BindingSource" /> is never overwritten.</item>
18-
/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>) is assigned <see cref="BindingSource.Body"/>.</item>
19+
/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), registered in the DI container, is assigned <see cref="BindingSource.Services"/>.</item>
20+
/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), not registered in the DI container, is assigned <see cref="BindingSource.Body"/>.</item>
1921
/// <item>Parameter with a name that appears as a route value in ANY route template is assigned <see cref="BindingSource.Path"/>.</item>
2022
/// <item>All other parameters are <see cref="BindingSource.Query"/>.</item>
2123
/// </list>
2224
/// </remarks>
2325
public class InferParameterBindingInfoConvention : IActionModelConvention
2426
{
2527
private readonly IModelMetadataProvider _modelMetadataProvider;
28+
private readonly IServiceProviderIsService? _serviceProviderIsService;
2629

2730
/// <summary>
2831
/// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>.
@@ -34,6 +37,21 @@ public InferParameterBindingInfoConvention(
3437
_modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
3538
}
3639

40+
/// <summary>
41+
/// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>.
42+
/// </summary>
43+
/// <param name="modelMetadataProvider">The model metadata provider.</param>
44+
/// <param name="serviceProviderIsService">The service to determine if the a type is available from the <see cref="IServiceProvider"/>.</param>
45+
public InferParameterBindingInfoConvention(
46+
IModelMetadataProvider modelMetadataProvider,
47+
IServiceProviderIsService serviceProviderIsService)
48+
: this(modelMetadataProvider)
49+
{
50+
_serviceProviderIsService = serviceProviderIsService ?? throw new ArgumentNullException(nameof(serviceProviderIsService));
51+
}
52+
53+
internal bool IsInferForServiceParametersEnabled => _serviceProviderIsService != null;
54+
3755
/// <summary>
3856
/// Called to determine whether the action should apply.
3957
/// </summary>
@@ -95,6 +113,11 @@ internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
95113
{
96114
if (IsComplexTypeParameter(parameter))
97115
{
116+
if (_serviceProviderIsService?.IsService(parameter.ParameterType) is true)
117+
{
118+
return BindingSource.Services;
119+
}
120+
98121
return BindingSource.Body;
99122
}
100123

src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.DisableImplicitFromServicesParameters.get -> bool
3+
Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.DisableImplicitFromServicesParameters.set -> void
4+
Microsoft.AspNetCore.Mvc.ApplicationModels.InferParameterBindingInfoConvention.InferParameterBindingInfoConvention(Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider! modelMetadataProvider, Microsoft.Extensions.DependencyInjection.IServiceProviderIsService! serviceProviderIsService) -> void
25
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider
36
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
47
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void

src/Mvc/Mvc.Core/test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Reflection;
@@ -138,6 +138,17 @@ public void Constructor_DoesNotAddInferParameterBindingInfoConvention_IfSuppress
138138
Assert.Empty(provider.ActionModelConventions.OfType<InferParameterBindingInfoConvention>());
139139
}
140140

141+
[Fact]
142+
public void Constructor_DoesNotInferServicesParameterBindingInfoConvention_IfSuppressInferBindingSourcesForParametersIsSet()
143+
{
144+
// Arrange
145+
var provider = GetProvider(new ApiBehaviorOptions { DisableImplicitFromServicesParameters = true });
146+
147+
// Act & Assert
148+
var convention = (InferParameterBindingInfoConvention)Assert.Single(provider.ActionModelConventions, c => c is InferParameterBindingInfoConvention);
149+
Assert.False(convention.IsInferForServiceParametersEnabled);
150+
}
151+
141152
[Fact]
142153
public void Constructor_DoesNotSpecifyDefaultErrorType_IfSuppressMapClientErrorsIsSet()
143154
{
@@ -163,7 +174,8 @@ private static ApiBehaviorApplicationModelProvider GetProvider(
163174
optionsAccessor,
164175
new EmptyModelMetadataProvider(),
165176
Mock.Of<IClientErrorFactory>(),
166-
loggerFactory);
177+
loggerFactory,
178+
Mock.Of<IServiceProvider>());
167179
}
168180

169181
private class TestApiController : ControllerBase

src/Mvc/Mvc.Core/test/ApplicationModels/InferParameterBindingInfoConventionTest.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel;
55
using System.Reflection;
66
using Microsoft.AspNetCore.Http;
77
using Microsoft.AspNetCore.Mvc.ModelBinding;
88
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
9+
using Microsoft.Extensions.DependencyInjection;
910
using Microsoft.Extensions.Options;
11+
using Moq;
1012

1113
namespace Microsoft.AspNetCore.Mvc.ApplicationModels;
1214

@@ -477,6 +479,24 @@ public void InferBindingSourceForParameter_ReturnsBodyForCollectionOfComplexType
477479
Assert.Same(BindingSource.Body, result);
478480
}
479481

482+
[Fact]
483+
public void InferBindingSourceForParameter_ReturnsServicesForComplexTypesRegisteredInDI()
484+
{
485+
// Arrange
486+
var actionName = nameof(ParameterBindingController.ServiceParameter);
487+
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
488+
// Using any built-in type defined in the Test action
489+
var serviceProvider = Mock.Of<IServiceProviderIsService>(s => s.IsService(typeof(IApplicationModelProvider)) == true);
490+
var convention = GetConvention(serviceProviderIsService: serviceProvider);
491+
492+
// Act
493+
var result = convention.InferBindingSourceForParameter(parameter);
494+
495+
// Assert
496+
Assert.True(convention.IsInferForServiceParametersEnabled);
497+
Assert.Same(BindingSource.Services, result);
498+
}
499+
480500
[Fact]
481501
public void PreservesBindingSourceInference_ForFromQueryParameter_WithDefaultName()
482502
{
@@ -732,10 +752,12 @@ public void PreservesBindingSourceInference_ForParameterWithRequestPredicateAndP
732752
}
733753

734754
private static InferParameterBindingInfoConvention GetConvention(
735-
IModelMetadataProvider modelMetadataProvider = null)
755+
IModelMetadataProvider modelMetadataProvider = null,
756+
IServiceProviderIsService serviceProviderIsService = null)
736757
{
737758
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
738-
return new InferParameterBindingInfoConvention(modelMetadataProvider);
759+
serviceProviderIsService = serviceProviderIsService ?? Mock.Of<IServiceProviderIsService>(s => s.IsService(It.IsAny<Type>()) == false);
760+
return new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
739761
}
740762

741763
private static ApplicationModelProviderContext GetContext(
@@ -871,6 +893,8 @@ private class ParameterBindingController
871893
public IActionResult CollectionOfSimpleTypes(IList<int> parameter) => null;
872894

873895
public IActionResult CollectionOfComplexTypes(IList<TestModel> parameter) => null;
896+
897+
public IActionResult ServiceParameter(IApplicationModelProvider parameter) => null;
874898
}
875899

876900
[ApiController]

src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,21 @@ private async Task ActionsWithApiBehaviorInferFromBodyParameters(string action)
145145
Assert.Equal(input.Name, result.Name);
146146
}
147147

148+
[Fact]
149+
public async Task ActionsWithApiBehavior_InferFromServicesParameters()
150+
{
151+
// Arrange
152+
var id = 1;
153+
var url = $"/contact/ActionWithInferredFromServicesParameter/{id}";
154+
var response = await Client.GetAsync(url);
155+
156+
// Assert
157+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
158+
var result = JsonConvert.DeserializeObject<Contact>(await response.Content.ReadAsStringAsync());
159+
Assert.NotNull(result);
160+
Assert.Equal(id, result.ContactId);
161+
}
162+
148163
[Fact]
149164
public async Task ActionsWithApiBehavior_InferQueryAndRouteParameters()
150165
{

src/Mvc/test/WebSites/BasicWebSite/Controllers/ContactApiController.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public ActionResult<string> ActionWithInferredModelBinderTypeWithExplicitModelNa
8383
return foo;
8484
}
8585

86+
[HttpGet("[action]/{id}")]
87+
public ActionResult<Contact> ActionWithInferredFromServicesParameter(int id, ContactsRepository repository)
88+
=> repository.GetContact(id) ?? new Contact() { ContactId = id };
89+
8690
[HttpGet("[action]")]
8791
public ActionResult<int> ActionReturningStatusCodeResult()
8892
{

0 commit comments

Comments
 (0)