Added support for controller action parameter validation into the ValidateModelStateAttribute (#7156)

This commit is contained in:
Max K 2017-12-14 05:26:57 +01:00 committed by William Cheng
parent 5306372b98
commit a96bc3a409
9 changed files with 98 additions and 18 deletions

View File

@ -125,8 +125,8 @@ public class AspNetCoreServerCodegen extends AbstractCSharpCodegen {
supportingFiles.add(new SupportingFile("Properties" + File.separator + "launchSettings.json", packageFolder + File.separator + "Properties", "launchSettings.json")); 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 + "BasePathFilter.mustache", packageFolder + File.separator + "Filters", "BasePathFilter.cs"));
supportingFiles.add(new SupportingFile("Filters" + File.separator + "PathParameterValidationFilter.mustache", packageFolder + File.separator + "Filters", "PathParameterValidationFilter.cs")); supportingFiles.add(new SupportingFile("Filters" + File.separator + "GeneratePathParamsValidationFilter.mustache", packageFolder + File.separator + "Filters", "GeneratePathParamsValidationFilter.cs"));
supportingFiles.add(new SupportingFile("wwwroot" + File.separator + "README.md", packageFolder + File.separator + "wwwroot", "README.md")); 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 + "index.html", packageFolder + File.separator + "wwwroot", "index.html"));

View File

@ -8,13 +8,13 @@ namespace {{packageName}}.Filters
/// <summary> /// <summary>
/// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths /// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths
/// </summary> /// </summary>
public class BasePathDocumentFilter : IDocumentFilter public class BasePathFilter : IDocumentFilter
{ {
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
/// <param name="basePath">BasePath to remove from Operations</param> /// <param name="basePath">BasePath to remove from Operations</param>
public BasePathDocumentFilter(string basePath) public BasePathFilter(string basePath)
{ {
BasePath = basePath; BasePath = basePath;
} }

View File

@ -7,9 +7,9 @@ using Swashbuckle.AspNetCore.SwaggerGen;
namespace {{packageName}}.Filters namespace {{packageName}}.Filters
{ {
/// <summary> /// <summary>
/// Path Parameter Validation Filter /// Path Parameter Validation Rules Filter
/// </summary> /// </summary>
public class PathParameterValidationFilter : IOperationFilter public class GeneratePathParamsValidationFilter : IOperationFilter
{ {
/// <summary> /// <summary>
/// Constructor /// Constructor

View File

@ -72,10 +72,12 @@ namespace {{packageName}}
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml"); c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml");
{{#basePathWithoutHost}} {{#basePathWithoutHost}}
// Sets the basePath property in the Swagger document generated // Sets the basePath property in the Swagger document generated
c.DocumentFilter<BasePathDocumentFilter>("{{{basePathWithoutHost}}}"); c.DocumentFilter<BasePathFilter>("{{{basePathWithoutHost}}}");
{{/basePathWithoutHost}} {{/basePathWithoutHost}}
// Do validation of path parameters w. DataAnnotation attributes
c.OperationFilter<PathParameterValidationFilter>(); // Include DataAnnotation attributes on Controller Action parameters as Swagger validation rules (e.g required, pattern, ..)
// Use [ValidateModelState] on Actions to actually validate it in C# as well!
c.OperationFilter<GeneratePathParamsValidationFilter>();
}); });
} }

View File

@ -1,5 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace {{packageName}}.Attributes namespace {{packageName}}.Attributes
{ {
@ -14,10 +18,44 @@ namespace {{packageName}}.Attributes
/// <param name="context"></param> /// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
// Per https://blog.markvincze.com/how-to-validate-action-parameters-with-dataannotation-attributes/
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
foreach (var parameter in descriptor.MethodInfo.GetParameters())
{
object args = null;
if (context.ActionArguments.ContainsKey(parameter.Name))
{
args = context.ActionArguments[parameter.Name];
}
ValidateAttributes(parameter, args, context.ModelState);
}
}
if (!context.ModelState.IsValid) if (!context.ModelState.IsValid)
{ {
context.Result = new BadRequestObjectResult(context.ModelState); context.Result = new BadRequestObjectResult(context.ModelState);
} }
} }
private void ValidateAttributes(ParameterInfo parameter, object args, ModelStateDictionary modelState)
{
foreach (var attributeData in parameter.CustomAttributes)
{
var attributeInstance = parameter.GetCustomAttribute(attributeData.AttributeType);
var validationAttribute = attributeInstance as ValidationAttribute;
if (validationAttribute != null)
{
var isValid = validationAttribute.IsValid(args);
if (!isValid)
{
modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
}
}
}
}
} }
} }

View File

@ -1,5 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace IO.Swagger.Attributes namespace IO.Swagger.Attributes
{ {
@ -14,10 +18,44 @@ namespace IO.Swagger.Attributes
/// <param name="context"></param> /// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
// Per https://blog.markvincze.com/how-to-validate-action-parameters-with-dataannotation-attributes/
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
foreach (var parameter in descriptor.MethodInfo.GetParameters())
{
object args = null;
if (context.ActionArguments.ContainsKey(parameter.Name))
{
args = context.ActionArguments[parameter.Name];
}
ValidateAttributes(parameter, args, context.ModelState);
}
}
if (!context.ModelState.IsValid) if (!context.ModelState.IsValid)
{ {
context.Result = new BadRequestObjectResult(context.ModelState); context.Result = new BadRequestObjectResult(context.ModelState);
} }
} }
private void ValidateAttributes(ParameterInfo parameter, object args, ModelStateDictionary modelState)
{
foreach (var attributeData in parameter.CustomAttributes)
{
var attributeInstance = parameter.GetCustomAttribute(attributeData.AttributeType);
var validationAttribute = attributeInstance as ValidationAttribute;
if (validationAttribute != null)
{
var isValid = validationAttribute.IsValid(args);
if (!isValid)
{
modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
}
}
}
}
} }
} }

View File

@ -8,13 +8,13 @@ namespace IO.Swagger.Filters
/// <summary> /// <summary>
/// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths /// BasePath Document Filter sets BasePath property of Swagger and removes it from the individual URL paths
/// </summary> /// </summary>
public class BasePathDocumentFilter : IDocumentFilter public class BasePathFilter : IDocumentFilter
{ {
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
/// <param name="basePath">BasePath to remove from Operations</param> /// <param name="basePath">BasePath to remove from Operations</param>
public BasePathDocumentFilter(string basePath) public BasePathFilter(string basePath)
{ {
BasePath = basePath; BasePath = basePath;
} }

View File

@ -7,9 +7,9 @@ using Swashbuckle.AspNetCore.SwaggerGen;
namespace IO.Swagger.Filters namespace IO.Swagger.Filters
{ {
/// <summary> /// <summary>
/// Path Parameter Validation Filter /// Path Parameter Validation Rules Filter
/// </summary> /// </summary>
public class PathParameterValidationFilter : IOperationFilter public class GeneratePathParamsValidationFilter : IOperationFilter
{ {
/// <summary> /// <summary>
/// Constructor /// Constructor

View File

@ -80,9 +80,11 @@ namespace IO.Swagger
c.DescribeAllEnumsAsStrings(); c.DescribeAllEnumsAsStrings();
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml"); c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{_hostingEnv.ApplicationName}.xml");
// Sets the basePath property in the Swagger document generated // Sets the basePath property in the Swagger document generated
c.DocumentFilter<BasePathDocumentFilter>("/v2"); c.DocumentFilter<BasePathFilter>("/v2");
// Do validation of path parameters w. DataAnnotation attributes
c.OperationFilter<PathParameterValidationFilter>(); // Include DataAnnotation attributes on Controller Action parameters as Swagger validation rules (e.g required, pattern, ..)
// Use [ValidateModelState] on Actions to actually validate it in C# as well!
c.OperationFilter<GeneratePathParamsValidationFilter>();
}); });
} }