Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit f2af66b

Browse files
authored
Cleanup InferParameterBindingInfoConvention (#8665)
* Cleanup InferParameterBindingInfoConvention * Infer BindingSource for collection parameters as Body. Fixes #8536 * Introduce a compat switch to keep 2.1.x LTS behavior for collection parameters * Do not infer BinderModelName in InferParameterBindingInfoConvention
1 parent c74a945 commit f2af66b

File tree

9 files changed

+180
-299
lines changed

9 files changed

+180
-299
lines changed

src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,9 @@ public virtual ModelMetadata ContainerMetadata
349349
/// Gets a value indicating whether <see cref="ModelType"/> is a complex type.
350350
/// </summary>
351351
/// <remarks>
352-
/// A complex type is defined as a <see cref="Type"/> which has a
353-
/// <see cref="TypeConverter"/> that can convert from <see cref="string"/>.
352+
/// A complex type is defined as a <see cref="Type"/> without a <see cref="TypeConverter"/> that can convert
353+
/// from <see cref="string"/>. Most POCO and <see cref="IEnumerable"/> types are therefore complex. Most, if
354+
/// not all, BCL value types are simple types.
354355
/// </remarks>
355356
public bool IsComplexType { get; private set; }
356357

src/Microsoft.AspNetCore.Mvc.Core/ApiBehaviorOptions.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
1717
{
1818
private readonly CompatibilitySwitch<bool> _suppressMapClientErrors;
1919
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
20+
private readonly CompatibilitySwitch<bool> _allowInferringBindingSourceForCollectionTypesAsFromQuery;
2021
private readonly ICompatibilitySwitch[] _switches;
2122

2223
private Func<ActionContext, IActionResult> _invalidModelStateResponseFactory;
@@ -28,10 +29,12 @@ public ApiBehaviorOptions()
2829
{
2930
_suppressMapClientErrors = new CompatibilitySwitch<bool>(nameof(SuppressMapClientErrors));
3031
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
32+
_allowInferringBindingSourceForCollectionTypesAsFromQuery = new CompatibilitySwitch<bool>(nameof(AllowInferringBindingSourceForCollectionTypesAsFromQuery));
3133
_switches = new[]
3234
{
3335
_suppressMapClientErrors,
3436
_suppressUseValidationProblemDetailsForInvalidModelStateResponses,
37+
_allowInferringBindingSourceForCollectionTypesAsFromQuery
3538
};
3639
}
3740

@@ -151,6 +154,43 @@ public bool SuppressUseValidationProblemDetailsForInvalidModelStateResponses
151154
set => _suppressUseValidationProblemDetailsForInvalidModelStateResponses.Value = value;
152155
}
153156

157+
/// <summary>
158+
/// Gets or sets a value that determines if <see cref="BindingInfo.BindingSource"/> for collection types
159+
/// (<see cref="ModelMetadata.IsCollectionType"/>).
160+
/// <para>
161+
/// When <see langword="true" />, the binding source for collection types is inferred as <see cref="BindingSource.Query"/>.
162+
/// Otherwise <see cref="BindingSource.Body"/> is inferred.
163+
/// </para>
164+
/// </summary>
165+
/// <value>
166+
/// The default value is <see langword="false"/> if the version is
167+
/// <see cref="CompatibilityVersion.Version_2_2"/> or later; <see langword="true"/> otherwise.
168+
/// </value>
169+
/// <remarks>
170+
/// <para>
171+
/// This property is associated with a compatibility switch and can provide a different behavior depending on
172+
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
173+
/// guidance and examples of setting the application's compatibility version.
174+
/// </para>
175+
/// <para>
176+
/// Configuring the desired value of the compatibility switch by calling this property's setter takes
177+
/// precedence over the value implied by the application's <see cref="CompatibilityVersion"/>.
178+
/// </para>
179+
/// <para>
180+
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
181+
/// lower then this setting will have the value <see langword="true"/> unless explicitly configured.
182+
/// </para>
183+
/// <para>
184+
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
185+
/// higher then this setting will have the value <see langword="false"/> unless explicitly configured.
186+
/// </para>
187+
/// </remarks>
188+
public bool AllowInferringBindingSourceForCollectionTypesAsFromQuery
189+
{
190+
get => _allowInferringBindingSourceForCollectionTypesAsFromQuery.Value;
191+
set => _allowInferringBindingSourceForCollectionTypesAsFromQuery.Value = value;
192+
}
193+
154194
/// <summary>
155195
/// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
156196
/// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>

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

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ public ApiBehaviorApplicationModelProvider(
4848
var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType);
4949
ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute));
5050

51-
var inferParameterBindingInfoConvention = new InferParameterBindingInfoConvention(modelMetadataProvider)
51+
if (!options.SuppressInferBindingSourcesForParameters)
5252
{
53-
SuppressInferBindingSourcesForParameters = options.SuppressInferBindingSourcesForParameters
54-
};
55-
ControllerModelConventions = new List<IControllerModelConvention>
56-
{
57-
inferParameterBindingInfoConvention,
58-
};
53+
var convention = new InferParameterBindingInfoConvention(modelMetadataProvider)
54+
{
55+
AllowInferringBindingSourceForCollectionTypesAsFromQuery = options.AllowInferringBindingSourceForCollectionTypesAsFromQuery,
56+
};
57+
58+
ActionModelConventions.Add(convention);
59+
}
5960
}
6061

6162
/// <remarks>
@@ -66,8 +67,6 @@ public ApiBehaviorApplicationModelProvider(
6667

6768
public List<IActionModelConvention> ActionModelConventions { get; }
6869

69-
public List<IControllerModelConvention> ControllerModelConventions { get; }
70-
7170
public void OnProvidersExecuted(ApplicationModelProviderContext context)
7271
{
7372
}
@@ -91,11 +90,6 @@ public void OnProvidersExecuting(ApplicationModelProviderContext context)
9190
convention.Apply(action);
9291
}
9392
}
94-
95-
foreach (var convention in ControllerModelConventions)
96-
{
97-
convention.Apply(controller);
98-
}
9993
}
10094
}
10195

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

Lines changed: 29 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Linq;
7-
using Microsoft.AspNetCore.Http;
86
using Microsoft.AspNetCore.Mvc.Internal;
97
using Microsoft.AspNetCore.Mvc.ModelBinding;
108
using Microsoft.AspNetCore.Routing.Template;
@@ -13,13 +11,18 @@
1311
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
1412
{
1513
/// <summary>
16-
/// A <see cref="IControllerModelConvention"/> that
17-
/// <list type="bullet">
18-
/// <item>infers binding sources for parameters</item>
19-
/// <item><see cref="BindingInfo.BinderModelName"/> for bound properties and parameters.</item>
20-
/// </list>
14+
/// An <see cref="IActionModelConvention"/> that infers <see cref="BindingInfo.BindingSource"/> for parameters.
2115
/// </summary>
22-
public class InferParameterBindingInfoConvention : IControllerModelConvention
16+
/// <remarks>
17+
/// The goal of this covention is to make intuitive and easy to document <see cref="BindingSource"/> inferences. The rules are:
18+
/// <list type="number">
19+
/// <item>A previously specified <see cref="BindingInfo.BindingSource" /> is never overwritten.</item>
20+
/// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>) is assigned <see cref="BindingSource.Body"/>.</item>
21+
/// <item>Parameter with a name that appears as a route value in ANY route template is assigned <see cref="BindingSource.Path"/>.</item>
22+
/// <item>All other parameters are <see cref="BindingSource.Query"/>.</item>
23+
/// </list>
24+
/// </remarks>
25+
public class InferParameterBindingInfoConvention : IActionModelConvention
2326
{
2427
private readonly IModelMetadataProvider _modelMetadataProvider;
2528

@@ -29,41 +32,27 @@ public InferParameterBindingInfoConvention(
2932
_modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
3033
}
3134

32-
/// <summary>
33-
/// Gets or sets a value that determines if model binding sources are inferred for action parameters on controllers is suppressed.
34-
/// </summary>
35-
public bool SuppressInferBindingSourcesForParameters { get; set; }
35+
internal bool AllowInferringBindingSourceForCollectionTypesAsFromQuery { get; set; }
3636

37-
protected virtual bool ShouldApply(ControllerModel controller) => true;
37+
protected virtual bool ShouldApply(ActionModel action) => true;
3838

39-
public void Apply(ControllerModel controller)
39+
public void Apply(ActionModel action)
4040
{
41-
if (controller == null)
41+
if (action == null)
4242
{
43-
throw new ArgumentNullException(nameof(controller));
43+
throw new ArgumentNullException(nameof(action));
4444
}
4545

46-
if (!ShouldApply(controller))
46+
if (!ShouldApply(action))
4747
{
4848
return;
4949
}
5050

51-
InferBoundPropertyModelPrefixes(controller);
52-
53-
foreach (var action in controller.Actions)
54-
{
55-
InferParameterBindingSources(action);
56-
InferParameterModelPrefixes(action);
57-
}
51+
InferParameterBindingSources(action);
5852
}
5953

6054
internal void InferParameterBindingSources(ActionModel action)
6155
{
62-
if (SuppressInferBindingSourcesForParameters)
63-
{
64-
return;
65-
}
66-
6756
for (var i = 0; i < action.Parameters.Count; i++)
6857
{
6958
var parameter = action.Parameters[i];
@@ -95,56 +84,17 @@ internal void InferParameterBindingSources(ActionModel action)
9584
// Internal for unit testing.
9685
internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
9786
{
98-
if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
87+
if (IsComplexTypeParameter(parameter))
9988
{
100-
return BindingSource.Path;
89+
return BindingSource.Body;
10190
}
10291

103-
var bindingSource = IsComplexTypeParameter(parameter) ?
104-
BindingSource.Body :
105-
BindingSource.Query;
106-
107-
return bindingSource;
108-
}
109-
110-
// For any complex types that are bound from value providers, set the prefix
111-
// to the empty prefix by default. This makes binding much more predictable
112-
// and describable via ApiExplorer
113-
internal void InferBoundPropertyModelPrefixes(ControllerModel controllerModel)
114-
{
115-
foreach (var property in controllerModel.ControllerProperties)
92+
if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
11693
{
117-
if (property.BindingInfo != null &&
118-
property.BindingInfo.BinderModelName == null &&
119-
property.BindingInfo.BindingSource != null &&
120-
!property.BindingInfo.BindingSource.IsGreedy &&
121-
!IsFormFile(property.ParameterType))
122-
{
123-
var metadata = _modelMetadataProvider.GetMetadataForProperty(
124-
controllerModel.ControllerType,
125-
property.PropertyInfo.Name);
126-
if (metadata.IsComplexType && !metadata.IsCollectionType)
127-
{
128-
property.BindingInfo.BinderModelName = string.Empty;
129-
}
130-
}
94+
return BindingSource.Path;
13195
}
132-
}
13396

134-
internal void InferParameterModelPrefixes(ActionModel action)
135-
{
136-
foreach (var parameter in action.Parameters)
137-
{
138-
var bindingInfo = parameter.BindingInfo;
139-
if (bindingInfo?.BindingSource != null &&
140-
bindingInfo.BinderModelName == null &&
141-
!bindingInfo.BindingSource.IsGreedy &&
142-
!IsFormFile(parameter.ParameterType) &&
143-
IsComplexTypeParameter(parameter))
144-
{
145-
parameter.BindingInfo.BinderModelName = string.Empty;
146-
}
147-
}
97+
return BindingSource.Query;
14898
}
14999

150100
private bool ParameterExistsInAnyRoute(ActionModel action, string parameterName)
@@ -171,13 +121,13 @@ private bool IsComplexTypeParameter(ParameterModel parameter)
171121
// No need for information from attributes on the parameter. Just use its type.
172122
var metadata = _modelMetadataProvider
173123
.GetMetadataForType(parameter.ParameterInfo.ParameterType);
174-
return metadata.IsComplexType && !metadata.IsCollectionType;
175-
}
176124

177-
private static bool IsFormFile(Type parameterType)
178-
{
179-
return typeof(IFormFile).IsAssignableFrom(parameterType) ||
180-
typeof(IEnumerable<IFormFile>).IsAssignableFrom(parameterType);
125+
if (AllowInferringBindingSourceForCollectionTypesAsFromQuery && metadata.IsCollectionType)
126+
{
127+
return false;
128+
}
129+
130+
return metadata.IsComplexType;
181131
}
182132
}
183133
}

src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorOptionsSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ protected override IReadOnlyDictionary<string, object> DefaultValues
3535
{
3636
dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true;
3737
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
38+
dictionary[nameof(ApiBehaviorOptions.AllowInferringBindingSourceForCollectionTypesAsFromQuery)] = true;
3839
}
3940

4041
return dictionary;

test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,8 @@ public void Constructor_SetsUpConventions()
9898
{
9999
var convention = Assert.IsType<ApiConventionApplicationModelConvention>(c);
100100
Assert.Equal(typeof(ProblemDetails), convention.DefaultErrorResponseType.Type);
101-
});
102-
103-
Assert.Collection(
104-
provider.ControllerModelConventions,
105-
c =>
106-
{
107-
var convention = Assert.IsType<InferParameterBindingInfoConvention>(c);
108-
Assert.False(convention.SuppressInferBindingSourcesForParameters);
109-
});
101+
},
102+
c => Assert.IsType<InferParameterBindingInfoConvention>(c));
110103
}
111104

112105
[Fact]
@@ -140,14 +133,13 @@ public void Constructor_DoesNotAddConsumesConstraintForFormFileParameterConventi
140133
}
141134

142135
[Fact]
143-
public void Constructor_SetsSuppressInferBindingSourcesForParametersIsSet()
136+
public void Constructor_DoesNotAddInferParameterBindingInfoConvention_IfSuppressInferBindingSourcesForParametersIsSet()
144137
{
145138
// Arrange
146139
var provider = GetProvider(new ApiBehaviorOptions { SuppressInferBindingSourcesForParameters = true });
147140

148141
// Act & Assert
149-
var convention = Assert.Single(provider.ControllerModelConventions.OfType<InferParameterBindingInfoConvention>());
150-
Assert.True(convention.SuppressInferBindingSourcesForParameters);
142+
Assert.Empty(provider.ActionModelConventions.OfType<InferParameterBindingInfoConvention>());
151143
}
152144

153145
[Fact]

0 commit comments

Comments
 (0)