Skip to content

Commit 23369c7

Browse files
committed
Fixup OpenAPI displayed param type when using TryParse
1 parent 28a9fc2 commit 23369c7

File tree

2 files changed

+49
-13
lines changed

2 files changed

+49
-13
lines changed

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
147147

148148
private ApiParameterDescription? CreateApiParameterDescription(ParameterInfo parameter, RoutePattern pattern)
149149
{
150-
var (source, name, allowEmpty) = GetBindingSourceAndName(parameter, pattern);
150+
var (source, name, allowEmpty, paramType) = GetBindingSourceAndName(parameter, pattern);
151151

152152
// Services are ignored because they are not request parameters.
153153
// We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
@@ -165,7 +165,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
165165
return new ApiParameterDescription
166166
{
167167
Name = name,
168-
ModelMetadata = CreateModelMetadata(parameter.ParameterType),
168+
ModelMetadata = CreateModelMetadata(paramType),
169169
Source = source,
170170
DefaultValue = parameter.DefaultValue,
171171
Type = parameter.ParameterType,
@@ -184,25 +184,25 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
184184

185185
// TODO: Share more of this logic with RequestDelegateFactory.CreateArgument(...) using RequestDelegateFactoryUtilities
186186
// which is shared source.
187-
private (BindingSource, string, bool) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern)
187+
private (BindingSource, string, bool, Type) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern)
188188
{
189189
var attributes = parameter.GetCustomAttributes();
190190

191191
if (attributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
192192
{
193-
return (BindingSource.Path, routeAttribute.Name ?? parameter.Name ?? string.Empty, false);
193+
return (BindingSource.Path, routeAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
194194
}
195195
else if (attributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
196196
{
197-
return (BindingSource.Query, queryAttribute.Name ?? parameter.Name ?? string.Empty, false);
197+
return (BindingSource.Query, queryAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
198198
}
199199
else if (attributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
200200
{
201-
return (BindingSource.Header, headerAttribute.Name ?? parameter.Name ?? string.Empty, false);
201+
return (BindingSource.Header, headerAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
202202
}
203203
else if (attributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } fromBodyAttribute)
204204
{
205-
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty);
205+
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty, parameter.ParameterType);
206206
}
207207
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) ||
208208
parameter.ParameterType == typeof(HttpContext) ||
@@ -213,23 +213,25 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
213213
ParameterBindingMethodCache.HasBindAsyncMethod(parameter) ||
214214
_serviceProviderIsService?.IsService(parameter.ParameterType) == true)
215215
{
216-
return (BindingSource.Services, parameter.Name ?? string.Empty, false);
216+
return (BindingSource.Services, parameter.Name ?? string.Empty, false, parameter.ParameterType);
217217
}
218218
else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter))
219219
{
220+
// complex types will display as strings since they use custom parsing via TryParse on a string
221+
var displayType = !parameter.ParameterType.IsPrimitive ? typeof(string) : parameter.ParameterType;
220222
// Path vs query cannot be determined by RequestDelegateFactory at startup currently because of the layering, but can be done here.
221223
if (parameter.Name is { } name && pattern.GetParameter(name) is not null)
222224
{
223-
return (BindingSource.Path, name, false);
225+
return (BindingSource.Path, name, false, displayType);
224226
}
225227
else
226228
{
227-
return (BindingSource.Query, parameter.Name ?? string.Empty, false);
229+
return (BindingSource.Query, parameter.Name ?? string.Empty, false, displayType);
228230
}
229231
}
230232
else
231233
{
232-
return (BindingSource.Body, parameter.Name ?? string.Empty, false);
234+
return (BindingSource.Body, parameter.Name ?? string.Empty, false, parameter.ParameterType);
233235
}
234236
}
235237

src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,47 @@ static void AssertPathParameter(ApiDescription apiDescription)
266266
}
267267

268268
[Fact]
269-
public void AddsFromRouteParameterAsPathWithCustomType()
269+
public void AddsFromRouteParameterAsPathWithCustomClassWithTryParse()
270270
{
271271
static void AssertPathParameter(ApiDescription apiDescription)
272272
{
273273
var param = Assert.Single(apiDescription.ParameterDescriptions);
274274
Assert.Equal(typeof(TryParseStringRecord), param.Type);
275-
Assert.Equal(typeof(TryParseStringRecord), param.ModelMetadata.ModelType);
275+
Assert.Equal(typeof(string), param.ModelMetadata.ModelType);
276276
Assert.Equal(BindingSource.Path, param.Source);
277277
}
278278

279279
AssertPathParameter(GetApiDescription((TryParseStringRecord foo) => { }, "/{foo}"));
280280
}
281281

282+
[Fact]
283+
public void AddsFromRouteParameterAsPathWithCustomTypeWithTryParse()
284+
{
285+
static void AssertPathParameter(ApiDescription apiDescription)
286+
{
287+
var param = Assert.Single(apiDescription.ParameterDescriptions);
288+
Assert.Equal(typeof(int), param.Type);
289+
Assert.Equal(typeof(int), param.ModelMetadata.ModelType);
290+
Assert.Equal(BindingSource.Path, param.Source);
291+
}
292+
293+
AssertPathParameter(GetApiDescription((int foo) => { }, "/{foo}"));
294+
}
295+
296+
[Fact]
297+
public void AddsFromRouteParameterAsPathWithStructTypeWithTryParse()
298+
{
299+
static void AssertPathParameter(ApiDescription apiDescription)
300+
{
301+
var param = Assert.Single(apiDescription.ParameterDescriptions);
302+
Assert.Equal(typeof(TryParseStringRecordStruct), param.Type);
303+
Assert.Equal(typeof(string), param.ModelMetadata.ModelType);
304+
Assert.Equal(BindingSource.Path, param.Source);
305+
}
306+
307+
AssertPathParameter(GetApiDescription((TryParseStringRecordStruct foo) => { }, "/{foo}"));
308+
}
309+
282310
[Fact]
283311
public void AddsFromQueryParameterAsQuery()
284312
{
@@ -918,6 +946,12 @@ public static bool TryParse(string value, out TryParseStringRecord result) =>
918946
throw new NotImplementedException();
919947
}
920948

949+
private record struct TryParseStringRecordStruct(int Value)
950+
{
951+
public static bool TryParse(string value, out TryParseStringRecordStruct result) =>
952+
throw new NotImplementedException();
953+
}
954+
921955
private record BindAsyncRecord(int Value)
922956
{
923957
public static ValueTask<BindAsyncRecord> BindAsync(HttpContext context, ParameterInfo parameter) =>

0 commit comments

Comments
 (0)