Skip to content

Commit 27d3226

Browse files
committed
Fixup OpenAPI displayed param type when using TryParse
1 parent d8a4aba commit 27d3226

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
@@ -149,7 +149,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
149149

150150
private ApiParameterDescription? CreateApiParameterDescription(ParameterInfo parameter, RoutePattern pattern)
151151
{
152-
var (source, name, allowEmpty) = GetBindingSourceAndName(parameter, pattern);
152+
var (source, name, allowEmpty, paramType) = GetBindingSourceAndName(parameter, pattern);
153153

154154
// Services are ignored because they are not request parameters.
155155
// We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
@@ -166,7 +166,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
166166
return new ApiParameterDescription
167167
{
168168
Name = name,
169-
ModelMetadata = CreateModelMetadata(parameter.ParameterType),
169+
ModelMetadata = CreateModelMetadata(paramType),
170170
Source = source,
171171
DefaultValue = parameter.DefaultValue,
172172
Type = parameter.ParameterType,
@@ -176,25 +176,25 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
176176

177177
// TODO: Share more of this logic with RequestDelegateFactory.CreateArgument(...) using RequestDelegateFactoryUtilities
178178
// which is shared source.
179-
private (BindingSource, string, bool) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern)
179+
private (BindingSource, string, bool, Type) GetBindingSourceAndName(ParameterInfo parameter, RoutePattern pattern)
180180
{
181181
var attributes = parameter.GetCustomAttributes();
182182

183183
if (attributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
184184
{
185-
return (BindingSource.Path, routeAttribute.Name ?? parameter.Name ?? string.Empty, false);
185+
return (BindingSource.Path, routeAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
186186
}
187187
else if (attributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
188188
{
189-
return (BindingSource.Query, queryAttribute.Name ?? parameter.Name ?? string.Empty, false);
189+
return (BindingSource.Query, queryAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
190190
}
191191
else if (attributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
192192
{
193-
return (BindingSource.Header, headerAttribute.Name ?? parameter.Name ?? string.Empty, false);
193+
return (BindingSource.Header, headerAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
194194
}
195195
else if (attributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } fromBodyAttribute)
196196
{
197-
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty);
197+
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty, parameter.ParameterType);
198198
}
199199
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) ||
200200
parameter.ParameterType == typeof(HttpContext) ||
@@ -205,23 +205,25 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
205205
ParameterBindingMethodCache.HasBindAsyncMethod(parameter) ||
206206
_serviceProviderIsService?.IsService(parameter.ParameterType) == true)
207207
{
208-
return (BindingSource.Services, parameter.Name ?? string.Empty, false);
208+
return (BindingSource.Services, parameter.Name ?? string.Empty, false, parameter.ParameterType);
209209
}
210210
else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter))
211211
{
212+
// complex types will display as strings since they use custom parsing via TryParse on a string
213+
var displayType = !parameter.ParameterType.IsPrimitive ? typeof(string) : parameter.ParameterType;
212214
// Path vs query cannot be determined by RequestDelegateFactory at startup currently because of the layering, but can be done here.
213215
if (parameter.Name is { } name && pattern.GetParameter(name) is not null)
214216
{
215-
return (BindingSource.Path, name, false);
217+
return (BindingSource.Path, name, false, displayType);
216218
}
217219
else
218220
{
219-
return (BindingSource.Query, parameter.Name ?? string.Empty, false);
221+
return (BindingSource.Query, parameter.Name ?? string.Empty, false, displayType);
220222
}
221223
}
222224
else
223225
{
224-
return (BindingSource.Body, parameter.Name ?? string.Empty, false);
226+
return (BindingSource.Body, parameter.Name ?? string.Empty, false, parameter.ParameterType);
225227
}
226228
}
227229

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

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

266266
[Fact]
267-
public void AddsFromRouteParameterAsPathWithCustomTypep()
267+
public void AddsFromRouteParameterAsPathWithCustomClassWithTryParse()
268268
{
269269
static void AssertPathParameter(ApiDescription apiDescription)
270270
{
271271
var param = Assert.Single(apiDescription.ParameterDescriptions);
272272
Assert.Equal(typeof(TryParseStringRecord), param.Type);
273-
Assert.Equal(typeof(TryParseStringRecord), param.ModelMetadata.ModelType);
273+
Assert.Equal(typeof(string), param.ModelMetadata.ModelType);
274274
Assert.Equal(BindingSource.Path, param.Source);
275275
}
276276

277277
AssertPathParameter(GetApiDescription((TryParseStringRecord foo) => { }, "/{foo}"));
278278
}
279279

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

921+
private record struct TryParseStringRecordStruct(int Value)
922+
{
923+
public static bool TryParse(string value, out TryParseStringRecordStruct result) =>
924+
throw new NotImplementedException();
925+
}
926+
893927
private record BindAsyncRecord(int Value)
894928
{
895929
public static ValueTask<BindAsyncRecord> BindAsync(HttpContext context, ParameterInfo parameter) =>

0 commit comments

Comments
 (0)