ASP.NET Core 7 Web API How to check request before controller?

Cenk 1,036 Reputation points
2023-07-31T11:23:51.3566667+00:00

Hi There,

I am working on an ASP.NET Core 7 Web API. I need to improve on hardcoded points like the one below. Here is the request dto and I am checking the price for a product code. I wonder if I can make this check on a filter or handler before coming to the controller. (I am planning to read those product codes from DB) If so, is this a good practice?

Thank you.

public class RequestDto
    {
        [Required(ErrorMessage = "Please add Product Code to the request.")]
        [MaxLength(15, ErrorMessage = "The product code can not be more than 15 characters")]
        public string productCode { get; set; }

        [Required(ErrorMessage = "Please add Quantity to the request.")]
        [Range(1, 100, ErrorMessage = "The quantity must be added.(quantity = 1)")]
        public int quantity { get; set; }

        [SwaggerSchema(ReadOnly = true)]
        public string? shopNo { get; set; }

        [SwaggerSchema(ReadOnly = true)]
        public string? safeNo { get; set; }

        [SwaggerSchema(ReadOnly = true)]
        public string? cashierNo { get; set; }

        [SwaggerSchema(ReadOnly = true)]
        public string? clientTrxRef { get; set; }

        [RequiredIf(ErrorMessage = "Price is required for this product code.",
            Conditions = new[]
            {
                "OYNPLSEZP0003","OYNPLSEZP0004","OYNPLSEZP0020","OYNPLSEZP0021","OYNPLSEZP0023","OYNPLSEZP0033","OYNPLSEZP0038",
                "OYNPLSEZP0041","OYNPLSEZP0042","OYNPLSEZP0043","OYNPLSEZP0056","OYNPLSEZP0057","OYNPLSEZP0058","OYNPLSEZP0059",
                "OYNPLSEZP0070","OYNPLSEZP0071","OYNPLSEZP0072","OYNPLSEZP0073","OYNPLSEZP0064","OYNPLSEZP0081","OYNPLSEZP0085",
                "OYNPLSEZP0089","OYNPLSEZP0090","OYNPLSEZP0093","OYNPLSEZP0094","OYNPLSEZP0095","OYNPLSEZP0096","OYNPLSEZP0097"
            })]
        public string? price { get; set; }
    }


public class RequiredIfAttribute : ValidationAttribute
    {
        public string[] Conditions { get; set; }

        protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            
            var productCode = validationContext.ObjectType.GetProperty("productCode")!.GetValue(validationContext.ObjectInstance, null);

            foreach (var condition in Conditions)
            {
                if (productCode != null && productCode.Equals(condition))
                {
                    if (value == null || string.IsNullOrEmpty(value.ToString()))
                    {
                        return new ValidationResult(ErrorMessage);
                    }
                }
            }

            return ValidationResult.Success;
        }
    }
Developer technologies ASP.NET ASP.NET Core
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Bruce (SqlWork.com) 77,686 Reputation points Volunteer Moderator
    2023-07-31T17:03:21.43+00:00

    as Validation Attributes do not support async validation, so I wouldn't put a database call in one. if the codes can be loaded into a static collection at startup, then ok.

    the controller should do this validation and add to the error state.

    0 comments No comments

  2. Anonymous
    2023-08-01T07:28:31.8233333+00:00

    Hi @Cenk

    You can try to use the following methods:

    Method 1: Access the DbContext in the RequiredIfAttribute, code like this:

        [AttributeUsage(AttributeTargets.Property)]
        public class RequiredIfAttribute : ValidationAttribute
        {
            private string PropertyName { get; set; }
            public RequiredIfAttribute(string propertyName) {
                PropertyName = propertyName;
            }
    
            protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
            {
                //get the productCode value.
                var productCode = validationContext.ObjectType.GetProperty(PropertyName)!.GetValue(validationContext.ObjectInstance, null);
                //get current dbcontext
                var dbcontext = validationContext.GetRequiredService<ApplicationDbContext>();
    
                    // using `dbcontext.ProductConditions.Any(c => c.Condition == productCode)` to
                    // check whether the database/productconditions table contains the specific product code.
                    if (productCode != null && dbcontext.ProductConditions.Any(c => c.Condition == productCode))
                    {
                        if (value == null || string.IsNullOrEmpty(value.ToString()))
                        {
                            return new ValidationResult(ErrorMessage);
                        }
                    }
                return ValidationResult.Success;
            }
        }
    

    and the RequestDto class looks as below:

        public class RequestDto 
        {
            [Required(ErrorMessage = "Please add Product Code to the request.")]
            [MaxLength(15, ErrorMessage = "The product code can not be more than 15 characters")]
            public string productCode { get; set; }
    
            [Required(ErrorMessage = "Please add Quantity to the request.")]
            [Range(1, 100, ErrorMessage = "The quantity must be added.(quantity = 1)")]
            public int quantity { get; set; } 
            public string? shopNo { get; set; } 
            public string? safeNo { get; set; } 
            public string? cashierNo { get; set; } 
            public string? clientTrxRef { get; set; }
    
            [RequiredIf("productCode", ErrorMessage = "Price is required for this product code.")]
            public string? price { get; set; } 
        }
    

    Method 2: Using Action Filter to get and validate the model, then based on the result to return the ModelState or not.

    Create an Action filter:

        public class ValidateModel : IAsyncActionFilter
        {
            private readonly ApplicationDbContext _dbContext;
    
            public ValidateModel(ApplicationDbContext applicationDbContext)
            {
                _dbContext=applicationDbContext;
            }
            public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
            {
                if (context.ModelState.IsValid)
                {
                    //get the model and the related model
                    var product = context.ActionArguments.Values.OfType<RequestDto>().Single();
                    var productcode = product.productCode;
                    var price = product.price;
    
                    if (productcode != null && _dbContext.ProductConditions.Any(c => c.Condition == productcode))
                    {
                        if (price == null || string.IsNullOrEmpty(price.ToString()))
                        {
                            context.ModelState.AddModelError("Price", "Price is required for this product code.");
                        }
                    } 
                }
    
                await next();
            }
        }
    

    Register the scope:

    builder.Services.AddScoped<ValidateModel>();
    

    Apply the Action Filter:

            [HttpPost]
            [ServiceFilter(typeof(ValidateModel))]
            public IActionResult Post(RequestDto item)
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
                return Ok(item);
            }
    

    The RequestDto class:

        public class RequestDto 
        {
            [Required(ErrorMessage = "Please add Product Code to the request.")]
            [MaxLength(15, ErrorMessage = "The product code can not be more than 15 characters")]
            public string productCode { get; set; }
    
            [Required(ErrorMessage = "Please add Quantity to the request.")]
            [Range(1, 100, ErrorMessage = "The quantity must be added.(quantity = 1)")]
            public int quantity { get; set; } 
            public string? shopNo { get; set; } 
            public string? safeNo { get; set; } 
            public string? cashierNo { get; set; } 
            public string? clientTrxRef { get; set; }
    
            //[RequiredIf("productCode", ErrorMessage = "Price is required for this product code.")]
            public string? price { get; set; } 
        }
    

    After running the application, the result as below:

    User's image


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Dillion

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.