-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
ApiResponseTypeProvider.cs
405 lines (350 loc) · 17.5 KB
/
ApiResponseTypeProvider.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// 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;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
internal sealed class ApiResponseTypeProvider
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IActionResultTypeMapper _mapper;
private readonly MvcOptions _mvcOptions;
public ApiResponseTypeProvider(
IModelMetadataProvider modelMetadataProvider,
IActionResultTypeMapper mapper,
MvcOptions mvcOptions)
{
_modelMetadataProvider = modelMetadataProvider;
_mapper = mapper;
_mvcOptions = mvcOptions;
}
public ICollection<ApiResponseType> GetApiResponseTypes(ControllerActionDescriptor action)
{
// We only provide response info if we can figure out a type that is a user-data type.
// Void /Task object/IActionResult will result in no data.
var declaredReturnType = GetDeclaredReturnType(action);
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
if (!HasSignificantMetadataProvider(responseMetadataAttributes) &&
action.Properties.TryGetValue(typeof(ApiConventionResult), out var result))
{
// Action does not have any conventions. Use conventions on it if present.
var apiConventionResult = (ApiConventionResult)result!;
responseMetadataAttributes.AddRange(apiConventionResult.ResponseMetadataProviders);
}
var defaultErrorType = typeof(void);
if (action.Properties.TryGetValue(typeof(ProducesErrorResponseTypeAttribute), out result))
{
defaultErrorType = ((ProducesErrorResponseTypeAttribute)result!).Type;
}
var producesResponseMetadata = action.EndpointMetadata.OfType<IProducesResponseTypeMetadata>().ToList();
var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, producesResponseMetadata, runtimeReturnType, defaultErrorType);
return apiResponseTypes;
}
private static List<IApiResponseMetadataProvider> GetResponseMetadataAttributes(ControllerActionDescriptor action)
{
if (action.FilterDescriptors == null)
{
return new List<IApiResponseMetadataProvider>();
}
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
// while searching for a filter that implements IApiResponseMetadataProvider.
//
// The workaround for that is to implement the metadata interface on the IFilterFactory.
return action.FilterDescriptors
.Select(fd => fd.Filter)
.OfType<IApiResponseMetadataProvider>()
.ToList();
}
private ICollection<ApiResponseType> GetApiResponseTypes(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
IReadOnlyList<IProducesResponseTypeMetadata> producesResponseMetadata,
Type? type,
Type defaultErrorType)
{
var contentTypes = new MediaTypeCollection();
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
var responseTypes = ReadResponseMetadata(
producesResponseMetadata,
type,
responseTypeMetadataProviders,
_modelMetadataProvider);
// Read response metadata from providers and
// overwrite responseTypes from the metadata based
// on the status code
var responseTypesFromProvider = ReadResponseMetadata(
responseMetadataAttributes,
type,
defaultErrorType,
contentTypes,
responseTypeMetadataProviders);
foreach (var responseType in responseTypesFromProvider)
{
responseTypes[responseType.Key] = responseType.Value;
}
// Set the default status only when no status has already been set explicitly
if (responseTypes.Count == 0 && type != null)
{
responseTypes.Add(StatusCodes.Status200OK, new ApiResponseType
{
StatusCode = StatusCodes.Status200OK,
Type = type,
});
}
if (contentTypes.Count == 0)
{
// None of the IApiResponseMetadataProvider specified a content type. This is common for actions that
// specify one or more ProducesResponseType but no ProducesAttribute. In this case, formatters will participate in conneg
// and respond to the incoming request.
// Querying IApiResponseTypeMetadataProvider.GetSupportedContentTypes with "null" should retrieve all supported
// content types that each formatter may respond in.
contentTypes.Add((string)null!);
}
foreach (var apiResponse in responseTypes.Values)
{
CalculateResponseFormatForType(apiResponse, contentTypes, responseTypeMetadataProviders, _modelMetadataProvider);
}
return responseTypes.Values;
}
// Shared with EndpointMetadataApiDescriptionProvider
internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
Type? type,
Type defaultErrorType,
MediaTypeCollection contentTypes,
IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders = null,
IModelMetadataProvider? modelMetadataProvider = null)
{
var results = new Dictionary<int, ApiResponseType>();
// Get the content type that the action explicitly set to support.
// Walk through all 'filter' attributes in order, and allow each one to see or override
// the results of the previous ones. This is similar to the execution path for content-negotiation.
if (responseMetadataAttributes != null)
{
foreach (var metadataAttribute in responseMetadataAttributes)
{
// All ProducesXAttributes, except for ProducesResponseTypeAttribute do
// not allow multiple instances on the same method/class/etc. For those
// scenarios, the `SetContentTypes` method on the attribute continuously
// clears out more general content types in favor of more specific ones
// since we iterate through the attributes in order. For example, if a
// Produces exists on both a controller and an action within the controller,
// we favor the definition in the action. This is a semantic that does not
// apply to ProducesResponseType, which allows multiple instances on an target.
if (metadataAttribute is not ProducesResponseTypeAttribute)
{
metadataAttribute.SetContentTypes(contentTypes);
}
var statusCode = metadataAttribute.StatusCode;
var apiResponseType = new ApiResponseType
{
Type = metadataAttribute.Type,
StatusCode = statusCode,
IsDefaultResponse = metadataAttribute is IApiDefaultResponseMetadataProvider,
};
if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
{
// ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified.
// In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a
// [ProducesResponseType(201)] instead of [ProducesResponseType(typeof(Person), 201] when typeof(Person) can be inferred
// from the return type.
apiResponseType.Type = type;
}
else if (IsClientError(statusCode))
{
// Determine whether or not the type was provided by the user. If so, favor it over the default
// error type for 4xx client errors if no response type is specified..
var setByDefault = metadataAttribute is ProducesResponseTypeAttribute { IsResponseTypeSetByDefault: true };
apiResponseType.Type = setByDefault ? defaultErrorType : apiResponseType.Type;
}
else if (apiResponseType.IsDefaultResponse)
{
apiResponseType.Type = defaultErrorType;
}
}
// We special case the handling of ProcuesResponseTypeAttributes since
// multiple ProducesResponseTypeAttributes are permitted on a single
// action/controller/etc. In that scenario, instead of picking the most-specific
// set of content types (like we do with the Produces attribute above) we process
// the content types for each attribute independently.
if (metadataAttribute is ProducesResponseTypeAttribute)
{
var attributeContentTypes = new MediaTypeCollection();
metadataAttribute.SetContentTypes(attributeContentTypes);
CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, modelMetadataProvider);
}
if (apiResponseType.Type != null)
{
results[apiResponseType.StatusCode] = apiResponseType;
}
}
}
return results;
}
internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
IReadOnlyList<IProducesResponseTypeMetadata> responseMetadata,
Type? type,
IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders = null,
IModelMetadataProvider? modelMetadataProvider = null)
{
var results = new Dictionary<int, ApiResponseType>();
foreach (var metadata in responseMetadata)
{
var statusCode = metadata.StatusCode;
var apiResponseType = new ApiResponseType
{
Type = metadata.Type,
StatusCode = statusCode,
};
if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
{
// Allow setting the response type from the return type of the method if it has
// not been set explicitly by the method.
apiResponseType.Type = type;
}
}
var attributeContentTypes = new MediaTypeCollection();
if (metadata.ContentTypes != null)
{
foreach (var contentType in metadata.ContentTypes)
{
attributeContentTypes.Add(contentType);
}
}
CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, modelMetadataProvider);
if (apiResponseType.Type != null)
{
results[apiResponseType.StatusCode] = apiResponseType;
}
}
return results;
}
// Shared with EndpointMetadataApiDescriptionProvider
internal static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders, IModelMetadataProvider? modelMetadataProvider)
{
// If response formats have already been calculate for this type,
// then exit early. This avoids populating the ApiResponseFormat for
// types that have already been handled, specifically ProducesResponseTypes.
if (apiResponse.ApiResponseFormats.Count > 0)
{
return;
}
// Given the content-types that were declared for this action, determine the formatters that support the content-type for the given
// response type.
// 1. Responses that do not specify an type do not have any associated content-type. This usually is meant for status-code only responses such
// as return NotFound();
// 2. When a type is specified, use GetSupportedContentTypes to expand wildcards and get the range of content-types formatters support.
// 3. When no formatter supports the specified content-type, use the user specified value as is. This is useful in actions where the user
// dictates the content-type.
// e.g. [Produces("application/pdf")] Action() => FileStream("somefile.pdf", "application/pdf");
var responseType = apiResponse.Type;
if (responseType == null || responseType == typeof(void))
{
return;
}
apiResponse.ModelMetadata = modelMetadataProvider?.GetMetadataForType(responseType);
foreach (var contentType in declaredContentTypes)
{
var isSupportedContentType = false;
if (responseTypeMetadataProviders != null)
{
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
{
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
contentType,
responseType);
if (formatterSupportedContentTypes == null)
{
continue;
}
isSupportedContentType = true;
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
{
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
{
Formatter = (IOutputFormatter)responseTypeMetadataProvider,
MediaType = formatterSupportedContentType,
});
}
}
}
if (!isSupportedContentType && contentType != null)
{
// No output formatter was found that supports this content type. Add the user specified content type as-is to the result.
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
{
MediaType = contentType,
});
}
}
}
private Type? GetDeclaredReturnType(ControllerActionDescriptor action)
{
var declaredReturnType = action.MethodInfo.ReturnType;
if (declaredReturnType == typeof(void) ||
declaredReturnType == typeof(Task) ||
declaredReturnType == typeof(ValueTask))
{
return typeof(void);
}
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
var unwrappedType = declaredReturnType;
if (declaredReturnType.IsGenericType &&
(declaredReturnType.GetGenericTypeDefinition() == typeof(Task<>) || declaredReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))
{
unwrappedType = declaredReturnType.GetGenericArguments()[0];
}
// If the method is declared to return IActionResult, IResult or a derived class, that information
// isn't valuable to the formatter.
if (typeof(IActionResult).IsAssignableFrom(unwrappedType) ||
typeof(IResult).IsAssignableFrom(unwrappedType))
{
return null;
}
// If we get here, the type should be a user-defined data type or an envelope type
// like ActionResult<T>. The mapper service will unwrap envelopes.
unwrappedType = _mapper.GetResultDataType(unwrappedType);
return unwrappedType;
}
private static Type? GetRuntimeReturnType(Type? declaredReturnType)
{
// If we get here, then a filter didn't give us an answer, so we need to figure out if we
// want to use the declared return type.
//
// We've already excluded Task, void, and IActionResult at this point.
//
// If the action might return any object, then assume we don't know anything about it.
if (declaredReturnType == typeof(object))
{
return null;
}
return declaredReturnType;
}
private static bool IsClientError(int statusCode)
{
return statusCode >= 400 && statusCode < 500;
}
private static bool HasSignificantMetadataProvider(IReadOnlyList<IApiResponseMetadataProvider> providers)
{
for (var i = 0; i < providers.Count; i++)
{
var provider = providers[i];
if (provider is ProducesAttribute producesAttribute && producesAttribute.Type is null)
{
// ProducesAttribute that does not specify type is considered not significant.
continue;
}
// Any other IApiResponseMetadataProvider is considered significant
return true;
}
return false;
}
}