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.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
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;
}
}
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.
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:
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