Auto API Validation with FluentValidation - c#

I want to auto apply validation for some models on requests execution. E.g. if I have a fluent validator for model A it will apply, if I don't have validator for model B then nothing happens. I wrote code, but maybe somebody can advise a better solution.

Create base class and interface:
public interface IBaseValidationModel
{
public void Validate(object validator, IBaseValidationModel modelObj);
}
public abstract class BaseValidationModel<T> : IBaseValidationModel
{
public void Validate(object validator, IBaseValidationModel modelObj)
{
var instance = (IValidator<T>)validator;
var result = instance.Validate((T)modelObj);
if (!result.IsValid && result.Errors.Any())
{
throw new Exception("INVALID");
}
}
}
Create model and validator:
public class LogInModel : BaseValidationModel<LogInModel>
{
public string? Name { get; set; }
public string? Password { get; set; }
public string? Domain { get; set; }
}
public class LogInModelValidator : AbstractValidator<LogInModel>
{
public LogInModelValidator()
{
RuleLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Domain).NotNull().NotEmpty();
RuleFor(x => x.Name).NotNull().NotEmpty();
RuleFor(x => x.Password).NotNull().NotEmpty();
}
}
Create action filter attribute to apply it to controllers:
public class ModelValidatorAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
foreach (var actionArgument in context.ActionArguments)
{
//validate that model is having validator and resolve it
if (actionArgument.Value is IBaseValidationModel model)
{
var modelType = actionArgument.Value.GetType();
var genericType = typeof(IValidator<>).MakeGenericType(modelType);
var validator = context.HttpContext.RequestServices.GetService(genericType);
if (validator != null)
{
// execute validator to validate model
model.Validate(validator, model);
}
}
}
base.OnActionExecuting(context);
}
}
Controller:
[Route("account")]
[ModelValidator]
public class AccountController : ControllerBase
{
public IActionResult Post([FromBody]LogInModel model)
{
return Ok("ok);
}
}

Related

Override the response body of RequiredAttribute in DataAnnotation in .NET Core / .NET 5

After adding the required attribute to API Request model and then when the request body is invalid I get the response as
{
"type":"<type_URL>",
"title":"One or more validation errors occurred.",
"status":400,
"traceId":"<trace_id_guid>",
"errors":{
"Id":[
"Value is required"
]
}
}
I would like to alter this body to something like,
{
"status":400,
"errors":[
"Value is required"
]
}
Is it possible to override some function to get this result?
You can create a custom ValidationError model which contains the returned fields, then try to use the action filter to handle the Validation failure error response. Check the following sample code:
Create custom ValidationError model which contains the returned fields:
public class ValidationResultModel
{
public string status { get; }
public List<ValidationError> Errors { get; }
public ValidationResultModel(ModelStateDictionary modelState)
{
status = "400";
Errors = modelState.Keys
.SelectMany(key => modelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
.ToList();
}
}
public class ValidationError
{
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message)
{
Field = field != string.Empty ? field : null;
Message = message;
}
}
Create custom IActionResult. By default, when display the validation error, it will return BadRequestObjectResult and the HTTP status code is 400. Here we could change the Http Status code.
public class ValidationFailedResult : ObjectResult
{
public ValidationFailedResult(ModelStateDictionary modelState)
: base(new ValidationResultModel(modelState))
{
StatusCode = StatusCodes.Status400BadRequest;
}
}
Create Custom Action Filter attribute:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailedResult(context.ModelState);
}
}
}
Change the default response type to SerializableError in Startup.ConfigureServices:
services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var result = new ValidationFailedResult(context.ModelState);
// TODO: add `using System.Net.Mime;` to resolve MediaTypeNames
result.ContentTypes.Add(MediaTypeNames.Application.Json);
result.ContentTypes.Add(MediaTypeNames.Application.Xml);
return result;
};
});
Add the custom action filter (ValidateModel) at the action method or controller.
[Route("api/[controller]")]
[ApiController]
[ValidateModel]
public class ToDoController : ControllerBase
{
Create a Model with DataAnnotations:
public class User
{
public int UserId { get; set; }
[Required]
public string UserName { get; set; }
}
API controller:
[Route("api/[controller]")]
[ApiController]
[ValidateModel]
public class ToDoController : ControllerBase
{
[HttpPost("AddUser")]
public async Task<ActionResult> AddUser([FromBody] User model)
{
if (ModelState.IsValid)
{
var result = "userID: " + model.UserId + " UserName: " + model.UserName;
return Ok(result);
}
return BadRequest(ModelState);
}
After running the application, the result like this:

pass value returned from a method to action filter

I have _serviceOne injected into my controller that has a method which returns an int value. I'm trying to pass this value into my custom action filter.
This isn't working and I am getting the error: An object reference is required for the non-static field, method, or property 'NameController._serviceOne' where I try to set the Number = _serviceOne.GetIntNumber.
I am aware that I can access a value if it is inside the controller (eg: controller parameter, ViewBag, ViewData, a variable in the controller), but I want to pass the value to the CustomActionFilter filter's Number property.
The filter and service method work the way I want it to, but it won't let me pass the value from _serviceOne.GetIntNumber into the filter. Why is this not working, and how could I make it work?
NameController.cs:
public class NameController : Controller
{
private readonly ServiceOne _serviceOne;
public NameController(ServiceOne serviceOne)
{
_serviceOne = serviceOne;
}
[CustomActionFilter(Name = "CorrectName", Number = _serviceOne.GetIntNumber)] //does not work
[HttpGet]
public IActionResult Index()
{
return View();
}
}
CustomActionFilter.cs:
public class CustomActionFilter : ActionFilterAttribute
{
public string Name { get; set; }
public int Number { get; set; }
public override void OnActionExecuted(ActionExecutedContext context)
{
if (Name == "CorrectName" && Number == 1) {
RouteValueDictionary routeDictionary = new RouteValueDictionary { { "action", "SomeAction" }, { "controller", "NameController" } };
context.Result = new RedirectToRouteResult(routeDictionary);
}
base.OnActionExecuted(context);
}
}
Attributes are created at compile time so it's not possible to pass them run-time values in the constructor.
Instead, you can access NameController's instance of the service, like this:
public class NameController : Controller
{
private readonly ServiceOne _serviceOne;
public ServiceOne ServiceOne => _serviceOne;
public NameController(ServiceOne serviceOne)
{
_serviceOne = serviceOne;
}
}
public class CustomActionFilter : ActionFilterAttribute
{
public string Name { get; set; }
public int Number { get; set; }
public override void OnActionExecuted(ActionExecutedContext context)
{
var controller = context.Controller as NameController;
var service = controller.ServiceOne;
//Use the service here
}
}
See also Access a controller's property from an action filter

Conditional required based on Controller in ASP.NET Core

I would like to write conditional required, but condition depends on the controller where it is used.
I already have custom attribute MyRequiredIfNot. I just don't know how to get information about controller in IsValid method.
For example:
public class MyController1 : Controller
{
public ActionResult Method1(MyModel model)
{
//Name is required !!!
}
}
public class MyController2 : MyController1
{
public ActionResult SomeMethod(MyModel model)
{
//Name is NOT required !!!
}
}
public class MyModel
{
[MyRequiredIfNot(MyController2)
public string Name { get;set; }
}
And missing implementation:
public class MyRequiredIfNotAttribute : ValidationAttribute
{
public MyRequiredIfNotAttribute(Controller controller) { }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (/*came_from ?*/ is this.controller) //Missing !!!!
return ValidationResult.Success;
else
return base.IsValid(value, validationContext);
}
}
For retriving the Controller, you could try IActionContextAccessor.
Follow steps below:
Register IActionContextAccessor
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
//rest services
}
MyRequiredIfNotAttribute
public class MyRequiredIfNotAttribute : RequiredAttribute//ValidationAttribute
{
private Type _type;
public MyRequiredIfNotAttribute(Type type) {
_type = type;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var actionContext = validationContext.GetRequiredService<IActionContextAccessor>();
var controllerActionDescriptor = actionContext.ActionContext.ActionDescriptor as ControllerActionDescriptor;
var controllerTypeName = controllerActionDescriptor.ControllerTypeInfo.FullName;
if (_type.FullName == controllerTypeName)
{
return ValidationResult.Success;
}
else
{
return base.IsValid(value, validationContext);
}
}
}
Useage
public class MyModel
{
[MyRequiredIfNot(typeof(MyController))]
public string Name { get; set; }
}

Array size validation in C#

I have a controller method with array of integers input, which must not be null or more than 10 elements size. To validate input I 've made a class:
public class TestForm
{
[Required]
[MaxLength(10)]
public long[] feedIds { get; set; }
}
And controller method:
[HttpPost]
public async Task<IActionResult> DoSomeJob(TestForm form)
{
//Do some job
}
According to MSDN, System.ComponentModel.DataAnnotations.MaxLength can be used for array, but there is no validation, it gets null and array of any size. What am I doing wrong?
Here is what we use in one of our projects:
public class LengthAttribute : ValidationAttribute {
readonly int length;
public LengthAttribute(int length) {
this.length = length;
}
public override bool IsValid(object value) {
if (value is ICollection == false) { return false; }
return ((ICollection)value).Count == length;
}
}
On a property like the following:
public class CreateUserApiRequest : ApiRequest {
[DataMember]
[Length(128)]
[Description("クライアントキー")]
public byte[] clientKey { get; set; }
....
The MaxLength atribute works fine. The problem was in action filter. Here is the code:
services.AddMvc(options =>
{
options.Filters.Add(new MyValidationFilterAttribute());
//Some other code
}
public class MyValidationFilterAttribute: IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
return;
if (!RequestRecognizingUtils.IsMobileAppRequest(context.HttpContext.Request))
return; //Here all validation results are ignored
}
}
In OnActionExecuting method validation errors were ignored

How to inject or find validaters by entity type in fluent validation?

how inject or find validators by entity type in fluent validation?
i have following classes and want validate entities by fluent validation
public class BaseEntity {}
public class Article :BaseEntity
{
public string Name {get;set;}
}
public class ArticleValidator : AbstractValidator<Article>
{
public ArticleValidator()
{
RuleFor(x => x.Name ).NotEmpty().Length(0,512);
}
}
and have extentions for BaseEntity:
public static ValidationResult Validate(this BaseEntity entity)
{
//????and here how i can find entity validator by type of entity and validate it and return result.
}
public class ArticleService
{
public void AddArticle(Article aricle)
{
var result = article.Validate();
if(result.IsValid)
;.......
}
}
You need something like this
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentValidation;
using FluentValidation.Results;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
var c = new MyClass();
var result = Validate(c);
}
public static ValidationResult Validate(object c)
{
if (c == null) throw new ArgumentNullException("c");
var vt = typeof (AbstractValidator<>);
var et = c.GetType();
var evt = vt.MakeGenericType(et);
var validatorType = FindValidatorType(Assembly.GetExecutingAssembly(), evt);
var validatorInstance = (IValidator)Activator.CreateInstance(validatorType);
return validatorInstance.Validate(c);
}
public static Type FindValidatorType(Assembly assembly, Type evt)
{
if (assembly == null) throw new ArgumentNullException("assembly");
if (evt == null) throw new ArgumentNullException("evt");
return assembly.GetTypes().FirstOrDefault(t => t.IsSubclassOf(evt));
}
}
public class MyClassValidator : AbstractValidator<MyClass>
{
}
public class MyClass
{
}
}
Make sure your concrete validators have at least one parameterless constructor.
Good luck
Inject IServiceProvider _servicesCollection into the controller (or wherever you're trying to validate the model from)
Define the following method
public Task<ValidationResult> ValidateModelAsync(object model)
{
var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
var validator = (IValidator)_servicesCollection.GetRequiredService(validatorType);
var validationContextType = typeof(ValidationContext<>).MakeGenericType(model.GetType());
var validationContext = (IValidationContext)Activator.CreateInstance(validationContextType, new [] {model } );
return validator.ValidateAsync(validationContext);
}
Use
[HttpPost("api/todo")]
public async Task<IActionResult> Todo(RequestModel model)
{
var validaitonResult = await ValidateModelAsync(model);
...
}
public class ValidationAspect : MethodInterception
{
private Type _validatorType;
public ValidationAspect(Type validatorType)
{
if (!typeof(IValidator).IsAssignableFrom(validatorType))
{
throw new System.Exception("This is not a validation class.");
}
_validatorType = validatorType;
}
protected override void OnBefore(IInvocation invocation)
{
var validator = (IValidator)Activator.CreateInstance(_validatorType);
var entityType = _validatorType.BaseType.GetGenericArguments()[0];
var entities = invocation.Arguments.Where(t => t.GetType() == entityType);
foreach (var entity in entities)
{
ValidationTool.Validate(validator, entity);
}
}
}
public class BrandInsertDtoValidator : AbstractValidator<BrandInsertDto>
{
public BrandInsertDtoValidator()
{
RuleFor(p => p.Name).NotNull();
RuleFor(p => p.Name).NotEmpty();
RuleFor(p => p.Name).MaximumLength(50);
RuleFor(p => p.Name.Length).GreaterThanOrEqualTo(2);
}
}
public class BrandInsertDto : IDTO
{
public string Name { get; set; }
}

Categories