locked
Data Annotation Validation (Web API) - Issues | ActionFilterAttribute RRS feed

  • Question

  • User425811032 posted

    Hello

    I have experience a few issues while working with Data Annotations, I am using ASP.Net Core 2.1:

    So Let me try and explain the issues that I have:
    Example Model:

    public class Arbitary
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            [Required]
            public Nullable<Guid> AID { get; set; }
    
            [Display(Name = "Arbitary  Name")]
            [Required(ErrorMessage = "The {0} cannot be empty"), StringLength(32, MinimumLength = 3, ErrorMessage = "The {0} must be between {2} - {1} characters.")]
            public string ArbitaryName { get; set; }
    
            [Display(Name = "Arbitary Web")]
            [Required(ErrorMessage = "The {0} cannot be empty"), StringLength(32, MinimumLength = 3, ErrorMessage = "The {0} must be between {2} - {1} characters.")]
            public string ArbitaryWeb { get; set; }
    
            public Nullable<Guid> ArbitaryReferenceAID { get; set; }
    
            [Display(Name = "Arbitary Country")]
            [Required(ErrorMessage = "The {0} cannot be empty"), StringLength(2, MinimumLength = 2, ErrorMessage = "The {0} must be {1} characters.")]
            public string ArbitaryCountry { get; set; }
    
            public Nullable<Boolean> IsArbitary { get; set; }
    
            public Nullable<DateTime> EntryDate { get; set; }
        }

    Example Data Access Layer:

        public class ArbitarysRepository
        {
    
            public async Task<Arbitary> GetArbitaryExample()
            {
                Arbitary tempArbitary = new Arbitary { AID = null, ArbitaryName = "Test Static Entry", ArbitaryWeb = null, ArbitaryCountry = "1", IsArbitary = null, ArbitaryReferenceAID = null, EntryDate=null};
    
                Arbitary thisshouldthrowerror = tempArbitary; //This should trigger model validation //AID, Web and country should validate?
    
                return thisshouldthrowerror;
            }
    
            public async Task<Arbitary> CreateArbitaryExample(Arbitary arbitary)
            {
                Arbitary tempArbitary = arbitary;
    
    
                return tempArbitary;
            }
    
        }

    Example Arbitary Controller

    [Route("api/[controller]")]
        [ApiController]
        public class ArbitarysController : ControllerBase
        {
            private readonly ArbitarysRepository _repository;
    
            public ArbitarysController(ArbitarysRepository repository)
            {
                _repository = repository ?? throw new ArgumentNullException(nameof(repository));
            }
    
            // GET api/values/5
            [HttpGet]
            public async Task<ActionResult<Arbitary>> Get()
            {
                var response = await _repository.GetArbitaryExample();
                if (response == null) { return NotFound(); }
                return response;
            }
    
            // POST api/values
            [HttpPost]
            [ExceptProperties("AID,ArbitaryName,ArbitaryWeb,ArbitaryCountry")]
            public async Task<ActionResult<Arbitary>> Post([FromBody] Arbitary arbitary)
            {
                var response = await _repository.CreateArbitaryExample(arbitary);
                if (response == null) { return NotFound(); }
                return response;
            }
    
        }

    And a custom ActionFilterAttribute called ExceptProperties to ignore validation of certail model properties:

    public class ExceptPropertiesAttribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
        {
            private IEnumerable<string> _propertiesKeys;
    
            public ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)
            {
                Trace.WriteLine("ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)");
                if (!string.IsNullOrEmpty(commaSeperatedPropertiesKeys))
                {
                    Trace.WriteLine("Keys " + commaSeperatedPropertiesKeys);
                    this._propertiesKeys = commaSeperatedPropertiesKeys.Split(',');
                }
            }
    
            public override void OnActionExecuted(ActionExecutedContext actionContext)
            {
                Trace.WriteLine("OnActionExecuting(ActionExecutingContext actionContext)");
                if (this._propertiesKeys != null)
                {
                    foreach (var propertyKey in this._propertiesKeys)
                    {
                        Trace.WriteLine("OnActionExecuting Propkey: " + propertyKey.ToString() + "      Contains in collection" + actionContext.ModelState.ContainsKey(propertyKey));
                        if (actionContext.ModelState.ContainsKey(propertyKey))
                        {
                            Trace.WriteLine("actionContext.ModelState.Remove(propertyKey)");
                            actionContext.ModelState.Remove(propertyKey);
                        }
                    }
                }
            }
        }

    And registered the following in my Startup.cs under ConfigureServices

                services.AddScoped<ExceptPropertiesAttribute>();
                services.AddScoped<ArbitarysRepository>();

    So; what am I having issues with? here is the first problem - the custom ActionFilterAttribute's OnActionExecuted is not fireing and I am unable to to trace why - please let me know as I can absolutely not understand why the OnActionExecuted is never triggered when example; doing a Post from the Controller with following "[ExceptProperties("AID,ArbitaryName,ArbitaryWeb,ArbitaryCountry")]" defined ; as I curretly get the normal validation while posting following: "{ "aid": null, "arbitaryName": "Test Static Entry", "arbitaryWeb": null, "arbitaryReferenceAID": null, "arbitaryCountry": "1", "isArbitary": null, "entryDate": null }" and getting the "expected validation errors back": "{ "AID": [ "The AID field is required." ], "ArbitaryWeb": [ "The Arbitary Web cannot be empty" ], "ArbitaryCountry": [ "The Arbitary Country must be 2 characters." ] }"

    I1) The goal is that ExceptProperties will ignore the validation errors for values specified as some values are not needed and will be null (On a per controller basis)

    I2) Furthermore; notice the "Arbitary tempArbitary = new Arbitary {Values that will not pass validation}" in GetArbitaryExample() - shoud this not fail or how can I force it to validate here?

    I have been stuck on this for two days and would like to get the ExceptProperties solution to work; thank you in advance.

    Thursday, January 21, 2021 3:11 PM

Answers

  • User1686398519 posted

    Hi zassadgh,

    the custom ActionFilterAttribute's OnActionExecuted is not fireing and I am unable to to trace why
    The goal is that ExceptProperties will ignore the validation

    1. The [ApiController] attribute makes model validation errors automatically trigger an HTTP 400 response.
      1. In other words, after using [ApiController], when you request an action, if the data you send does not meet the verification information in the model, it will first verify and then return a 400 error.
      2. At this time, you will not enter the Filter defined by yourself.
    2. Also you need to pay attention: The OnActionExecuted method runs after the action method, OnActionExecuting is called before the action method.
    3. Regarding this issue, you need to set the SuppressModelStateInvalidFilter property to true to disable the automatic 400 behavior.
      1. Startup
        • public void ConfigureServices(IServiceCollection services)
          {
             services.Configure<ApiBehaviorOptions>(options =>
             {
          options.SuppressConsumesConstraintForFormFileParameters = true; options.SuppressInferBindingSourcesForParameters = true; options.SuppressModelStateInvalidFilter = true; }); }
      2. ExceptPropertiesAttribute
        •     public class ExceptPropertiesAttribute : ActionFilterAttribute
              {
                  private IEnumerable<string> _propertiesKeys;
          
                  public ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)
                  {
                      Trace.WriteLine("ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)");
                      if (!string.IsNullOrEmpty(commaSeperatedPropertiesKeys))
                      {
                          Trace.WriteLine("Keys " + commaSeperatedPropertiesKeys);
                          this._propertiesKeys = commaSeperatedPropertiesKeys.Split(',');
                      }
                  }
                  public override void OnActionExecuting(ActionExecutingContext actionContext)
                  {
                      var result = actionContext.ModelState.IsValid;
                      Trace.WriteLine("OnActionExecuting(ActionExecutingContext actionContext)");
                      if (this._propertiesKeys != null)
                      {
                          foreach (var propertyKey in this._propertiesKeys)
                          {
                              Trace.WriteLine("OnActionExecuting Propkey: " + propertyKey.ToString() + "      Contains in collection" + actionContext.ModelState.ContainsKey(propertyKey));
                              if (actionContext.ModelState.ContainsKey(propertyKey))
                              {
                                  Trace.WriteLine("actionContext.ModelState.Remove(propertyKey)");
                                  actionContext.ModelState.Remove(propertyKey);
                              }
                          }
                      }
                  }
              }

    Arbitary tempArbitary = new Arbitary {Values that will not pass validation}" in GetArbitaryExample() - shoud this not fail or how can I force it to validate here?

    1. You can use TryValidateModel to validate.
    2. [HttpGet]
      public async Task<ActionResult<Arbitary>> Get()
      {
         var response = await _repository.GetArbitaryExample();
         if (
      response == null) { return NotFound();
      } else{ var t = TryValidateModel(response); if (!t){ return BadRequest(); } } return response; }

    Here is the result.

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, January 22, 2021 9:45 AM

All replies

  • User1686398519 posted

    Hi zassadgh,

    the custom ActionFilterAttribute's OnActionExecuted is not fireing and I am unable to to trace why
    The goal is that ExceptProperties will ignore the validation

    1. The [ApiController] attribute makes model validation errors automatically trigger an HTTP 400 response.
      1. In other words, after using [ApiController], when you request an action, if the data you send does not meet the verification information in the model, it will first verify and then return a 400 error.
      2. At this time, you will not enter the Filter defined by yourself.
    2. Also you need to pay attention: The OnActionExecuted method runs after the action method, OnActionExecuting is called before the action method.
    3. Regarding this issue, you need to set the SuppressModelStateInvalidFilter property to true to disable the automatic 400 behavior.
      1. Startup
        • public void ConfigureServices(IServiceCollection services)
          {
             services.Configure<ApiBehaviorOptions>(options =>
             {
          options.SuppressConsumesConstraintForFormFileParameters = true; options.SuppressInferBindingSourcesForParameters = true; options.SuppressModelStateInvalidFilter = true; }); }
      2. ExceptPropertiesAttribute
        •     public class ExceptPropertiesAttribute : ActionFilterAttribute
              {
                  private IEnumerable<string> _propertiesKeys;
          
                  public ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)
                  {
                      Trace.WriteLine("ExceptPropertiesAttribute(string commaSeperatedPropertiesKeys)");
                      if (!string.IsNullOrEmpty(commaSeperatedPropertiesKeys))
                      {
                          Trace.WriteLine("Keys " + commaSeperatedPropertiesKeys);
                          this._propertiesKeys = commaSeperatedPropertiesKeys.Split(',');
                      }
                  }
                  public override void OnActionExecuting(ActionExecutingContext actionContext)
                  {
                      var result = actionContext.ModelState.IsValid;
                      Trace.WriteLine("OnActionExecuting(ActionExecutingContext actionContext)");
                      if (this._propertiesKeys != null)
                      {
                          foreach (var propertyKey in this._propertiesKeys)
                          {
                              Trace.WriteLine("OnActionExecuting Propkey: " + propertyKey.ToString() + "      Contains in collection" + actionContext.ModelState.ContainsKey(propertyKey));
                              if (actionContext.ModelState.ContainsKey(propertyKey))
                              {
                                  Trace.WriteLine("actionContext.ModelState.Remove(propertyKey)");
                                  actionContext.ModelState.Remove(propertyKey);
                              }
                          }
                      }
                  }
              }

    Arbitary tempArbitary = new Arbitary {Values that will not pass validation}" in GetArbitaryExample() - shoud this not fail or how can I force it to validate here?

    1. You can use TryValidateModel to validate.
    2. [HttpGet]
      public async Task<ActionResult<Arbitary>> Get()
      {
         var response = await _repository.GetArbitaryExample();
         if (
      response == null) { return NotFound();
      } else{ var t = TryValidateModel(response); if (!t){ return BadRequest(); } } return response; }

    Here is the result.

    Best Regards,

    YihuiSun

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, January 22, 2021 9:45 AM
  • User425811032 posted

    Thank you YihuiSun; very impressive answer.

    I've learned a lot from your reply and I thank you for your clear explanations.
    This does solve my question; testing it; it works but I am unable to get a function similarly to

    var t = TryValidateModel(response);

    to trigger the validation error messages; so that the api can return a response similarly to before(when automatic validation was on); like:
    { "AID": [ "The AID field is required." ], "ArbitaryWeb": [ "The Arbitary Web cannot be empty" ], "ArbitaryCountry": [ "The Arbitary Country must be 2 characters." ] }

    Do you by any chance know how to trigger this validation reponse so that above 'error' message is returned when my validation 'fails' on the values I have not excluded in the ExceptProperties?

    Thank you for your reply.

    Friday, January 22, 2021 10:58 AM
  • User1686398519 posted

    Hi zassadgh, 

    Do you by any chance know how to trigger this validation reponse so that above 'error' message is returned when my validation 'fails' on the values I have not excluded in the ExceptProperties?

    You can return a BadRequest with the ModelStateDictionary parameter.

            [HttpGet]
            public async Task<ActionResult<Arbitary>> Get()
            {
                var response = await _repository.GetArbitaryExample();
                if (response == null) { return NotFound(); } 
                else
                {
                    var t = TryValidateModel(response);
                    var result = ModelState.IsValid;
                    if (!t)
                    {
                        return BadRequest(ModelState);
                    }
                }
                return response;
            }
            [HttpPost]
            [ExceptProperties("AID,ArbitaryName,ArbitaryWeb,ArbitaryCountry")]
            public async Task<ActionResult<Arbitary>> Post([FromBody] Arbitary arbitary)
            {
                var result=ModelState.IsValid;
                if (result)
                {
                    var response = await _repository.CreateArbitaryExample(arbitary);
                    if (response == null) { 
                        return NotFound();
                    }
                    else
                    {
                        return response;
                    }
                }
                else
                {
                    return BadRequest(ModelState);
                }
            }

    Here is the result. 

    Best Regards,

    YihuiSun

    Monday, January 25, 2021 2:58 AM