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

Commit 3f54492

Browse files
committed
[Fixes #885] API Explorer: Link Generation
1) Expose the simplified relative path template by cleaning up constraints, optional and catch all tokens from the template. 2) Expose the parameters on the route template as API parameters. 3) Combine parameters from the route and the action descriptor when the parameter doesn't come from the body. #886 will refine this. 4) Expose optionality and constraints for path parameters. Open question: Should we explicitly expose IsCatchAll?
1 parent a633ef4 commit 3f54492

File tree

9 files changed

+746
-45
lines changed

9 files changed

+746
-45
lines changed

samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace MvcSample.Web.ApiExplorerSamples
99
[Route("api/Products")]
1010
public class ProductsController : Controller
1111
{
12-
[HttpGet("{id}")]
12+
[HttpGet("{id:int}")]
1313
public Product GetById(int id)
1414
{
1515
return null;
@@ -22,7 +22,7 @@ public IEnumerable<Product> SearchByName(string name)
2222
}
2323

2424
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
25-
[HttpPut("{id}/Buy")]
25+
[HttpPut("{id:int}/Buy")]
2626
public IActionResult Buy(int projectId, int quantity = 1)
2727
{
2828
return null;

samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
<ul>
1414
@foreach (var parameter in Model.ParameterDescriptions)
1515
{
16-
<li>@parameter.Name - @parameter.Type.FullName - @parameter.Source.ToString()</li>
16+
<li>
17+
@parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString()
18+
- Constraint: @(parameter?.Constraint?.GetType()?.Name?.Replace("RouteConstraint", "") ?? " none")
19+
- Default value: @(parameter?.DefaultValue ?? " none")
20+
</li>
1721
}
1822
</ul>
1923
}
@@ -22,7 +26,7 @@
2226
{
2327
<p>Response Formats:</p>
2428
<ul>
25-
@foreach(var response in Model.SupportedResponseFormats)
29+
@foreach (var response in Model.SupportedResponseFormats)
2630
{
2731
<li>@response.MediaType.RawValue - @response.Formatter.GetType().Name</li>
2832
}

src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using Microsoft.AspNet.Mvc.ModelBinding;
6+
using Microsoft.AspNet.Routing;
67

78
namespace Microsoft.AspNet.Mvc.Description
89
{
@@ -18,6 +19,10 @@ public class ApiParameterDescription
1819

1920
public ApiParameterSource Source { get; set; }
2021

22+
public IRouteConstraint Constraint { get; set; }
23+
24+
public object DefaultValue { get; set; }
25+
2126
public Type Type { get; set; }
2227
}
2328
}

src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public enum ApiParameterSource
88
{
99
Body,
1010
Query,
11+
Path
1112
}
1213
}

src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs

Lines changed: 171 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.Contracts;
67
using System.Linq;
78
using System.Threading.Tasks;
89
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
910
using Microsoft.AspNet.Mvc.ModelBinding;
11+
using Microsoft.AspNet.Routing;
12+
using Microsoft.AspNet.Routing.Template;
1013
using Microsoft.Framework.DependencyInjection;
1114

1215
namespace Microsoft.AspNet.Mvc.Description
@@ -19,6 +22,7 @@ public class DefaultApiDescriptionProvider : INestedProvider<ApiDescriptionProvi
1922
{
2023
private readonly IOutputFormattersProvider _formattersProvider;
2124
private readonly IModelMetadataProvider _modelMetadataProvider;
25+
private readonly IInlineConstraintResolver _constraintResolver;
2226

2327
/// <summary>
2428
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
@@ -27,10 +31,12 @@ public class DefaultApiDescriptionProvider : INestedProvider<ApiDescriptionProvi
2731
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
2832
public DefaultApiDescriptionProvider(
2933
IOutputFormattersProvider formattersProvider,
34+
IInlineConstraintResolver constraintResolver,
3035
IModelMetadataProvider modelMetadataProvider)
3136
{
3237
_formattersProvider = formattersProvider;
3338
_modelMetadataProvider = modelMetadataProvider;
39+
_constraintResolver = constraintResolver;
3440
}
3541

3642
/// <inheritdoc />
@@ -60,25 +66,23 @@ public void Invoke(ApiDescriptionProviderContext context, Action callNext)
6066
}
6167

6268
private ApiDescription CreateApiDescription(
63-
ControllerActionDescriptor action,
64-
string httpMethod,
69+
ControllerActionDescriptor action,
70+
string httpMethod,
6571
string groupName)
6672
{
73+
var parsedTemplate = ParseTemplate(action);
74+
6775
var apiDescription = new ApiDescription()
6876
{
6977
ActionDescriptor = action,
7078
GroupName = groupName,
7179
HttpMethod = httpMethod,
72-
RelativePath = GetRelativePath(action),
80+
RelativePath = GetRelativePath(parsedTemplate),
7381
};
7482

75-
if (action.Parameters != null)
76-
{
77-
foreach (var parameter in action.Parameters)
78-
{
79-
apiDescription.ParameterDescriptions.Add(GetParameter(parameter));
80-
}
81-
}
83+
var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List<TemplatePart>();
84+
85+
GetParameters(apiDescription, action.Parameters, templateParameters);
8286

8387
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
8488

@@ -103,13 +107,13 @@ private ApiDescription CreateApiDescription(
103107
apiDescription.ResponseType = runtimeReturnType;
104108

105109
apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(
106-
modelAccessor: null,
110+
modelAccessor: null,
107111
modelType: runtimeReturnType);
108112

109113
var formats = GetResponseFormats(
110-
action,
111-
responseMetadataAttributes,
112-
declaredReturnType,
114+
action,
115+
responseMetadataAttributes,
116+
declaredReturnType,
113117
runtimeReturnType);
114118

115119
foreach (var format in formats)
@@ -121,6 +125,44 @@ private ApiDescription CreateApiDescription(
121125
return apiDescription;
122126
}
123127

128+
private void GetParameters(
129+
ApiDescription apiDescription,
130+
IList<ParameterDescriptor> parameterDescriptors,
131+
IList<TemplatePart> templateParameters)
132+
{
133+
if (parameterDescriptors != null)
134+
{
135+
foreach (var parameter in parameterDescriptors)
136+
{
137+
// Process together parameters that appear on the path template and on the
138+
// action descriptor and do not come from the body.
139+
TemplatePart templateParameter = null;
140+
if (parameter.BodyParameterInfo == null)
141+
{
142+
templateParameter = templateParameters
143+
.FirstOrDefault(p => p.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase));
144+
145+
if (templateParameter != null)
146+
{
147+
templateParameters.Remove(templateParameter);
148+
}
149+
}
150+
151+
apiDescription.ParameterDescriptions.Add(GetParameter(parameter, templateParameter));
152+
}
153+
}
154+
155+
if (templateParameters.Count > 0)
156+
{
157+
// Process parameters that only appear on the path template if any.
158+
foreach (var templateParameter in templateParameters)
159+
{
160+
var parameterDescription = GetParameter(parameterDescriptor: null, templateParameter: templateParameter);
161+
apiDescription.ParameterDescriptions.Add(parameterDescription);
162+
}
163+
}
164+
}
165+
124166
private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
125167
{
126168
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
@@ -133,23 +175,93 @@ private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
133175
}
134176
}
135177

136-
private string GetRelativePath(ControllerActionDescriptor action)
178+
private RouteTemplate ParseTemplate(ControllerActionDescriptor action)
137179
{
138-
// This is a placeholder for functionality which will correctly generate the relative path
139-
// stub of an action. See: #885
140180
if (action.AttributeRouteInfo != null &&
141181
action.AttributeRouteInfo.Template != null)
142182
{
143-
return action.AttributeRouteInfo.Template;
183+
return TemplateParser.Parse(action.AttributeRouteInfo.Template, _constraintResolver);
144184
}
145185

146186
return null;
147187
}
148188

149-
private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
189+
private string GetRelativePath(RouteTemplate parsedTemplate)
190+
{
191+
if (parsedTemplate == null)
192+
{
193+
return null;
194+
}
195+
196+
var segments = new List<string>();
197+
198+
foreach (var segment in parsedTemplate.Segments)
199+
{
200+
var currentSegment = "";
201+
foreach (var part in segment.Parts)
202+
{
203+
if (part.IsLiteral)
204+
{
205+
currentSegment += part.Text;
206+
}
207+
else if (part.IsParameter)
208+
{
209+
currentSegment += "{" + part.Name + "}";
210+
}
211+
}
212+
213+
segments.Add(currentSegment);
214+
}
215+
216+
return string.Join("/", segments);
217+
}
218+
219+
private ApiParameterDescription GetParameter(
220+
ParameterDescriptor parameterDescriptor,
221+
TemplatePart templateParameter)
150222
{
151223
// This is a placeholder based on currently available functionality for parameters. See #886.
152-
var resourceParameter = new ApiParameterDescription()
224+
ApiParameterDescription parameterDescription = null;
225+
226+
if (templateParameter != null && parameterDescriptor == null)
227+
{
228+
// The parameter is part of the route template but not part of the ActionDescriptor.
229+
230+
// For now if a parameter is part of the template we will asume its value comes from the path.
231+
// We will be more accurate when we implement #886.
232+
parameterDescription = CreateParameterFromTemplate(templateParameter);
233+
}
234+
else if (templateParameter != null && parameterDescriptor != null)
235+
{
236+
// The parameter is part of the route template and part of the ActionDescriptor.
237+
parameterDescription = CreateParameterFromTemplateAndParameterDescriptor(
238+
templateParameter,
239+
parameterDescriptor);
240+
}
241+
else if(templateParameter == null && parameterDescriptor != null)
242+
{
243+
// The parameter is part of the ActionDescriptor but is not part of the route template.
244+
parameterDescription = CreateParameterFromParameterDescriptor(parameterDescriptor);
245+
}
246+
else
247+
{
248+
// We will never call this method with templateParameter == null && parameterDescriptor == null
249+
Contract.Assert(parameterDescriptor != null);
250+
}
251+
252+
if (parameterDescription.Type != null)
253+
{
254+
parameterDescription.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
255+
modelAccessor: null,
256+
modelType: parameterDescription.Type);
257+
}
258+
259+
return parameterDescription;
260+
}
261+
262+
private static ApiParameterDescription CreateParameterFromParameterDescriptor(ParameterDescriptor parameter)
263+
{
264+
var resourceParameter = new ApiParameterDescription
153265
{
154266
IsOptional = parameter.IsOptional,
155267
Name = parameter.Name,
@@ -158,8 +270,8 @@ private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
158270

159271
if (parameter.ParameterBindingInfo != null)
160272
{
161-
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
162273
resourceParameter.Source = ApiParameterSource.Query;
274+
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
163275
}
164276

165277
if (parameter.BodyParameterInfo != null)
@@ -168,16 +280,49 @@ private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
168280
resourceParameter.Source = ApiParameterSource.Body;
169281
}
170282

171-
if (resourceParameter.Type != null)
283+
return resourceParameter;
284+
}
285+
286+
private static ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor(
287+
TemplatePart templateParameter,
288+
ParameterDescriptor parameter)
289+
{
290+
var resourceParameter = new ApiParameterDescription
291+
{
292+
Source = ApiParameterSource.Path,
293+
IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter),
294+
Name = parameter.Name,
295+
ParameterDescriptor = parameter,
296+
Constraint = templateParameter.InlineConstraint,
297+
DefaultValue = templateParameter.DefaultValue,
298+
};
299+
300+
if (parameter.ParameterBindingInfo != null)
172301
{
173-
resourceParameter.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
174-
modelAccessor: null,
175-
modelType: resourceParameter.Type);
302+
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
176303
}
177304

178305
return resourceParameter;
179306
}
180307

308+
private static bool IsOptionalParameter(TemplatePart templateParameter)
309+
{
310+
return templateParameter.IsOptional || templateParameter.DefaultValue != null;
311+
}
312+
313+
private static ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter)
314+
{
315+
return new ApiParameterDescription
316+
{
317+
Source = ApiParameterSource.Path,
318+
IsOptional = IsOptionalParameter(templateParameter),
319+
Name = templateParameter.Name,
320+
ParameterDescriptor = null,
321+
Constraint = templateParameter.InlineConstraint,
322+
DefaultValue = templateParameter.DefaultValue,
323+
};
324+
}
325+
181326
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
182327
ControllerActionDescriptor action,
183328
IApiResponseMetadataProvider[] responseMetadataAttributes,
@@ -220,7 +365,7 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
220365
}
221366
}
222367
}
223-
}
368+
}
224369

225370
return results;
226371
}

0 commit comments

Comments
 (0)