Added Base Path support as well as PathParameterValidation (#7120)

* Ran bin/aspnetcore-petstore-server.sh

* Added BasePath filter and Validation filter

* Updated filters and added Attribute generators

* Removed newline

* Updated samples
This commit is contained in:
Max K 2017-12-11 17:04:20 +01:00 committed by William Cheng
parent 3c8293bbcf
commit d881cb39bf
15 changed files with 329 additions and 16 deletions

View File

@ -120,6 +120,9 @@ public class AspNetCoreServerCodegen extends AbstractCSharpCodegen {
supportingFiles.add(new SupportingFile("Properties" + File.separator + "launchSettings.json", packageFolder + File.separator + "Properties", "launchSettings.json"));
supportingFiles.add(new SupportingFile("Filters" + File.separator + "BasePathDocumentFilter.mustache", packageFolder + File.separator + "Filters", "BasePathDocumentFilter.cs"));
supportingFiles.add(new SupportingFile("Filters" + File.separator + "PathParameterValidationFilter.mustache", packageFolder + File.separator + "Filters", "PathParameterValidationFilter.cs"));
supportingFiles.add(new SupportingFile("wwwroot" + File.separator + "README.md", packageFolder + File.separator + "wwwroot", "README.md"));
supportingFiles.add(new SupportingFile("wwwroot" + File.separator + "index.html", packageFolder + File.separator + "wwwroot", "index.html"));
supportingFiles.add(new SupportingFile("wwwroot" + File.separator + "web.config", packageFolder + File.separator + "wwwroot", "web.config"));

View File

@ -0,0 +1,50 @@
using System.Linq;
using System.Text.RegularExpressions;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace {{packageName}}.Filters
{
/// <summary>
/// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths
/// </summary>
public class BasePathDocumentFilter : IDocumentFilter
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="basePath">BasePath to remove from Operations</param>
public BasePathDocumentFilter(string basePath)
{
BasePath = basePath;
}
/// <summary>
/// Gets the BasePath of the Swagger Doc
/// </summary>
/// <returns>The BasePath of the Swagger Doc</returns>
public string BasePath { get; }
/// <summary>
/// Apply the filter
/// </summary>
/// <param name="swaggerDoc">SwaggerDocument</param>
/// <param name="context">FilterContext</param>
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.BasePath = this.BasePath;
var pathsToModify = swaggerDoc.Paths.Where(p => p.Key.StartsWith(this.BasePath)).ToList();
foreach (var path in pathsToModify)
{
if (path.Key.StartsWith(this.BasePath))
{
string newKey = Regex.Replace(path.Key, $"^{this.BasePath}", string.Empty);
swaggerDoc.Paths.Remove(path.Key);
swaggerDoc.Paths.Add(newKey, path.Value);
}
}
}
}
}

View File

@ -0,0 +1,97 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace {{packageName}}.Filters
{
/// <summary>
/// Path Parameter Validation Filter
/// </summary>
public class PathParameterValidationFilter : IOperationFilter
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="operation">Operation</param>
/// <param name="context">OperationFilterContext</param>
public void Apply(Operation operation, OperationFilterContext context)
{
var pars = context.ApiDescription.ParameterDescriptions;
foreach (var par in pars)
{
var swaggerParam = operation.Parameters.SingleOrDefault(p => p.Name == par.Name);
var attributes = ((ControllerParameterDescriptor)par.ParameterDescriptor).ParameterInfo.CustomAttributes;
if (attributes != null && attributes.Count() > 0 && swaggerParam != null)
{
// Required - [Required]
var requiredAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RequiredAttribute));
if (requiredAttr != null)
{
swaggerParam.Required = true;
}
// Regex Pattern [RegularExpression]
var regexAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RegularExpressionAttribute));
if (regexAttr != null)
{
string regex = (string)regexAttr.ConstructorArguments[0].Value;
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).Pattern = regex;
}
}
// String Length [StringLength]
int? minLenght = null, maxLength = null;
var stringLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(StringLengthAttribute));
if (stringLengthAttr != null)
{
if (stringLengthAttr.NamedArguments.Count == 1)
{
minLenght = (int)stringLengthAttr.NamedArguments.Single(p => p.MemberName == "MinimumLength").TypedValue.Value;
}
maxLength = (int)stringLengthAttr.ConstructorArguments[0].Value;
}
var minLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(MinLengthAttribute));
if (minLengthAttr != null)
{
minLenght = (int)minLengthAttr.ConstructorArguments[0].Value;
}
var maxLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(MaxLengthAttribute));
if (maxLengthAttr != null)
{
maxLength = (int)maxLengthAttr.ConstructorArguments[0].Value;
}
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).MinLength = minLenght;
((NonBodyParameter)swaggerParam).MaxLength = maxLength;
}
// Range [Range]
var rangeAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RangeAttribute));
if (rangeAttr != null)
{
int rangeMin = (int)rangeAttr.ConstructorArguments[0].Value;
int rangeMax = (int)rangeAttr.ConstructorArguments[1].Value;
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).Minimum = rangeMin;
((NonBodyParameter)swaggerParam).Maximum = rangeMax;
}
}
}
}
}
}
}

View File

@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using {{packageName}}.Filters;
namespace {{packageName}}
{
@ -62,6 +63,12 @@ namespace {{packageName}}
c.CustomSchemaIds(type => type.FriendlyId(true));
c.DescribeAllEnumsAsStrings();
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml");
{{#basePathWithoutHost}}
// Sets the basePath property in the Swagger document generated
c.DocumentFilter<BasePathDocumentFilter>("{{{basePathWithoutHost}}}");
{{/basePathWithoutHost}}
// Do validation of path parameters w. DataAnnotation attributes
c.OperationFilter<PathParameterValidationFilter>();
});
}

View File

@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Swashbuckle.AspNetCore.SwaggerGen;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using {{packageName}}.Attributes;
using {{packageName}}.Models;

View File

@ -1 +1 @@
{{#isFormParam}}[FromForm]{{&dataType}} {{paramName}}{{/isFormParam}}
{{#isFormParam}}[FromForm]{{#required}}[Required()]{{/required}}{{#pattern}}[RegularExpression("{{{pattern}}}")]{{/pattern}}{{#minLength}}{{#maxLength}}[StringLength({{maxLength}}, MinimumLength={{minLength}}){{/maxLength}}{{/minLength}}{{#minLength}}{{^maxLength}} [MinLength({{minLength}})]{{/maxLength}}{{/minLength}}{{^minLength}}{{#maxLength}} [MaxLength({{maxLength}})]{{/maxLength}}{{/minLength}}{{#minimum}}{{#maximum}}[Range({{minimum}}, {{maximum}})]{{/maximum}}{{/minimum}}{{&dataType}} {{paramName}}{{/isFormParam}}

View File

@ -1 +1 @@
{{#isHeaderParam}}[FromHeader]{{&dataType}} {{paramName}}{{/isHeaderParam}}
{{#isHeaderParam}}[FromHeader]{{#required}}[Required()]{{/required}}{{#pattern}}[RegularExpression("{{{pattern}}}")]{{/pattern}}{{#minLength}}{{#maxLength}}[StringLength({{maxLength}}, MinimumLength={{minLength}}){{/maxLength}}{{/minLength}}{{#minLength}}{{^maxLength}} [MinLength({{minLength}})]{{/maxLength}}{{/minLength}}{{^minLength}}{{#maxLength}} [MaxLength({{maxLength}})]{{/maxLength}}{{/minLength}}{{#minimum}}{{#maximum}}[Range({{minimum}}, {{maximum}})]{{/maximum}}{{/minimum}}{{&dataType}} {{paramName}}{{/isHeaderParam}}

View File

@ -1 +1 @@
{{#isPathParam}}[FromRoute]{{&dataType}} {{paramName}}{{/isPathParam}}
{{#isPathParam}}[FromRoute]{{#required}}[Required]{{/required}}{{#pattern}}[RegularExpression("{{{pattern}}}")]{{/pattern}}{{#minLength}}{{#maxLength}}[StringLength({{maxLength}}, MinimumLength={{minLength}}){{/maxLength}}{{/minLength}}{{#minLength}}{{^maxLength}} [MinLength({{minLength}})]{{/maxLength}}{{/minLength}}{{^minLength}}{{#maxLength}} [MaxLength({{maxLength}})]{{/maxLength}}{{/minLength}}{{#minimum}}{{#maximum}}[Range({{minimum}}, {{maximum}})]{{/maximum}}{{/minimum}}{{&dataType}} {{paramName}}{{/isPathParam}}

View File

@ -1 +1 @@
{{#isQueryParam}}[FromQuery]{{&dataType}} {{paramName}}{{/isQueryParam}}
{{#isQueryParam}}[FromQuery]{{#required}}[Required()]{{/required}}{{#pattern}}[RegularExpression("{{{pattern}}}")]{{/pattern}}{{#minLength}}{{#maxLength}}[StringLength({{maxLength}}, MinimumLength={{minLength}}){{/maxLength}}{{/minLength}}{{#minLength}}{{^maxLength}} [MinLength({{minLength}})]{{/maxLength}}{{/minLength}}{{^minLength}}{{#maxLength}} [MaxLength({{maxLength}})]{{/maxLength}}{{/minLength}}{{#minimum}}{{#maximum}}[Range({{minimum}}, {{maximum}})]{{/maximum}}{{/minimum}}{{&dataType}} {{paramName}}{{/isQueryParam}}

View File

@ -20,6 +20,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Swashbuckle.AspNetCore.SwaggerGen;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes;
using IO.Swagger.Models;
@ -60,7 +61,7 @@ namespace IO.Swagger.Controllers
[Route("/v2/pet/{petId}")]
[ValidateModelState]
[SwaggerOperation("DeletePet")]
public virtual IActionResult DeletePet([FromRoute]long? petId, [FromHeader]string apiKey)
public virtual IActionResult DeletePet([FromRoute][Required]long? petId, [FromHeader]string apiKey)
{
//TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(400);
@ -82,7 +83,7 @@ namespace IO.Swagger.Controllers
[SwaggerOperation("FindPetsByStatus")]
[SwaggerResponse(200, typeof(List<Pet>), "successful operation")]
[SwaggerResponse(400, typeof(List<Pet>), "Invalid status value")]
public virtual IActionResult FindPetsByStatus([FromQuery]List<string> status)
public virtual IActionResult FindPetsByStatus([FromQuery][Required()]List<string> status)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(List<Pet>));
@ -112,7 +113,7 @@ namespace IO.Swagger.Controllers
[SwaggerOperation("FindPetsByTags")]
[SwaggerResponse(200, typeof(List<Pet>), "successful operation")]
[SwaggerResponse(400, typeof(List<Pet>), "Invalid tag value")]
public virtual IActionResult FindPetsByTags([FromQuery]List<string> tags)
public virtual IActionResult FindPetsByTags([FromQuery][Required()]List<string> tags)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(List<Pet>));
@ -144,7 +145,7 @@ namespace IO.Swagger.Controllers
[SwaggerResponse(200, typeof(Pet), "successful operation")]
[SwaggerResponse(400, typeof(Pet), "Invalid ID supplied")]
[SwaggerResponse(404, typeof(Pet), "Pet not found")]
public virtual IActionResult GetPetById([FromRoute]long? petId)
public virtual IActionResult GetPetById([FromRoute][Required]long? petId)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(Pet));
@ -203,7 +204,7 @@ namespace IO.Swagger.Controllers
[Route("/v2/pet/{petId}")]
[ValidateModelState]
[SwaggerOperation("UpdatePetWithForm")]
public virtual IActionResult UpdatePetWithForm([FromRoute]long? petId, [FromForm]string name, [FromForm]string status)
public virtual IActionResult UpdatePetWithForm([FromRoute][Required]long? petId, [FromForm]string name, [FromForm]string status)
{
//TODO: Uncomment the next line to return response 405 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(405);
@ -225,7 +226,7 @@ namespace IO.Swagger.Controllers
[ValidateModelState]
[SwaggerOperation("UploadFile")]
[SwaggerResponse(200, typeof(ApiResponse), "successful operation")]
public virtual IActionResult UploadFile([FromRoute]long? petId, [FromForm]string additionalMetadata, [FromForm]System.IO.Stream file)
public virtual IActionResult UploadFile([FromRoute][Required]long? petId, [FromForm]string additionalMetadata, [FromForm]System.IO.Stream file)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(ApiResponse));

View File

@ -20,6 +20,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Swashbuckle.AspNetCore.SwaggerGen;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes;
using IO.Swagger.Models;
@ -41,7 +42,7 @@ namespace IO.Swagger.Controllers
[Route("/v2/store/order/{orderId}")]
[ValidateModelState]
[SwaggerOperation("DeleteOrder")]
public virtual IActionResult DeleteOrder([FromRoute]string orderId)
public virtual IActionResult DeleteOrder([FromRoute][Required]string orderId)
{
//TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(400);
@ -92,7 +93,7 @@ namespace IO.Swagger.Controllers
[SwaggerResponse(200, typeof(Order), "successful operation")]
[SwaggerResponse(400, typeof(Order), "Invalid ID supplied")]
[SwaggerResponse(404, typeof(Order), "Order not found")]
public virtual IActionResult GetOrderById([FromRoute]long? orderId)
public virtual IActionResult GetOrderById([FromRoute][Required][Range(1, 5)]long? orderId)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(Order));

View File

@ -20,6 +20,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Swashbuckle.AspNetCore.SwaggerGen;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using IO.Swagger.Attributes;
using IO.Swagger.Models;
@ -98,7 +99,7 @@ namespace IO.Swagger.Controllers
[Route("/v2/user/{username}")]
[ValidateModelState]
[SwaggerOperation("DeleteUser")]
public virtual IActionResult DeleteUser([FromRoute]string username)
public virtual IActionResult DeleteUser([FromRoute][Required]string username)
{
//TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(400);
@ -125,7 +126,7 @@ namespace IO.Swagger.Controllers
[SwaggerResponse(200, typeof(User), "successful operation")]
[SwaggerResponse(400, typeof(User), "Invalid username supplied")]
[SwaggerResponse(404, typeof(User), "User not found")]
public virtual IActionResult GetUserByName([FromRoute]string username)
public virtual IActionResult GetUserByName([FromRoute][Required]string username)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(User));
@ -159,7 +160,7 @@ namespace IO.Swagger.Controllers
[SwaggerOperation("LoginUser")]
[SwaggerResponse(200, typeof(string), "successful operation")]
[SwaggerResponse(400, typeof(string), "Invalid username/password supplied")]
public virtual IActionResult LoginUser([FromQuery]string username, [FromQuery]string password)
public virtual IActionResult LoginUser([FromQuery][Required()]string username, [FromQuery][Required()]string password)
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200, default(string));
@ -206,7 +207,7 @@ namespace IO.Swagger.Controllers
[Route("/v2/user/{username}")]
[ValidateModelState]
[SwaggerOperation("UpdateUser")]
public virtual IActionResult UpdateUser([FromRoute]string username, [FromBody]User body)
public virtual IActionResult UpdateUser([FromRoute][Required]string username, [FromBody]User body)
{
//TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(400);

View File

@ -0,0 +1,50 @@
using System.Linq;
using System.Text.RegularExpressions;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace IO.Swagger.Filters
{
/// <summary>
/// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths
/// </summary>
public class BasePathDocumentFilter : IDocumentFilter
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="basePath">BasePath to remove from Operations</param>
public BasePathDocumentFilter(string basePath)
{
BasePath = basePath;
}
/// <summary>
/// Gets the BasePath of the Swagger Doc
/// </summary>
/// <returns>The BasePath of the Swagger Doc</returns>
public string BasePath { get; }
/// <summary>
/// Apply the filter
/// </summary>
/// <param name="swaggerDoc">SwaggerDocument</param>
/// <param name="context">FilterContext</param>
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.BasePath = this.BasePath;
var pathsToModify = swaggerDoc.Paths.Where(p => p.Key.StartsWith(this.BasePath)).ToList();
foreach (var path in pathsToModify)
{
if (path.Key.StartsWith(this.BasePath))
{
string newKey = Regex.Replace(path.Key, $"^{this.BasePath}", string.Empty);
swaggerDoc.Paths.Remove(path.Key);
swaggerDoc.Paths.Add(newKey, path.Value);
}
}
}
}
}

View File

@ -0,0 +1,97 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace IO.Swagger.Filters
{
/// <summary>
/// Path Parameter Validation Filter
/// </summary>
public class PathParameterValidationFilter : IOperationFilter
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="operation">Operation</param>
/// <param name="context">OperationFilterContext</param>
public void Apply(Operation operation, OperationFilterContext context)
{
var pars = context.ApiDescription.ParameterDescriptions;
foreach (var par in pars)
{
var swaggerParam = operation.Parameters.SingleOrDefault(p => p.Name == par.Name);
var attributes = ((ControllerParameterDescriptor)par.ParameterDescriptor).ParameterInfo.CustomAttributes;
if (attributes != null && attributes.Count() > 0 && swaggerParam != null)
{
// Required - [Required]
var requiredAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RequiredAttribute));
if (requiredAttr != null)
{
swaggerParam.Required = true;
}
// Regex Pattern [RegularExpression]
var regexAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RegularExpressionAttribute));
if (regexAttr != null)
{
string regex = (string)regexAttr.ConstructorArguments[0].Value;
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).Pattern = regex;
}
}
// String Length [StringLength]
int? minLenght = null, maxLength = null;
var stringLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(StringLengthAttribute));
if (stringLengthAttr != null)
{
if (stringLengthAttr.NamedArguments.Count == 1)
{
minLenght = (int)stringLengthAttr.NamedArguments.Single(p => p.MemberName == "MinimumLength").TypedValue.Value;
}
maxLength = (int)stringLengthAttr.ConstructorArguments[0].Value;
}
var minLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(MinLengthAttribute));
if (minLengthAttr != null)
{
minLenght = (int)minLengthAttr.ConstructorArguments[0].Value;
}
var maxLengthAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(MaxLengthAttribute));
if (maxLengthAttr != null)
{
maxLength = (int)maxLengthAttr.ConstructorArguments[0].Value;
}
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).MinLength = minLenght;
((NonBodyParameter)swaggerParam).MaxLength = maxLength;
}
// Range [Range]
var rangeAttr = attributes.FirstOrDefault(p => p.AttributeType == typeof(RangeAttribute));
if (rangeAttr != null)
{
int rangeMin = (int)rangeAttr.ConstructorArguments[0].Value;
int rangeMax = (int)rangeAttr.ConstructorArguments[1].Value;
if (swaggerParam is NonBodyParameter)
{
((NonBodyParameter)swaggerParam).Minimum = rangeMin;
((NonBodyParameter)swaggerParam).Maximum = rangeMax;
}
}
}
}
}
}
}

View File

@ -19,6 +19,7 @@ using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using IO.Swagger.Filters;
namespace IO.Swagger
{
@ -71,6 +72,10 @@ namespace IO.Swagger
c.CustomSchemaIds(type => type.FriendlyId(true));
c.DescribeAllEnumsAsStrings();
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml");
// Sets the basePath property in the Swagger document generated
c.DocumentFilter<BasePathDocumentFilter>("/v2");
// Do validation of path parameters w. DataAnnotation attributes
c.OperationFilter<PathParameterValidationFilter>();
});
}