I'm trying to use custom remote validation to validate 2 properties based on one another with no success.
Action
Edit only, no insert
Properties
Both properties are free text (no dropdownlist)
FolderName
FileName
Validation conditions
FileName can be empty even if folder name is present
FileName can only be filled in if folder name is present
Filename is unique for each unique folder
When edit page is loaded and in case if there are already data present then no check should be done unless the user modifies theses values.
Code
1- Json function in the controller
public JsonResult IsFileValid(string folderName, string fileName)
{
if (!folderName.IsNullOrEmpty() && fileName.IsNullOrEmpty())
{
// FileName can be empty even if Folder name is present
return Json(true, JsonRequestBehavior.AllowGet);
}
if (folderName.IsNullOrEmpty() && fileName.IsNullOrEmpty())
{
//FileName can be empty even if Folder name is present
return Json(true, JsonRequestBehavior.AllowGet);
}
if (folderName.IsNullOrEmpty() && !fileName.IsNullOrEmpty())
{
//FileName can only be filled in if Folder name is present
return Json(false, JsonRequestBehavior.AllowGet);
}
var Folder =
Uow.Folders.GetAll()
.FirstOrDefault(x => x.Name.ToLower().Trim() == folderName.Trim().ToLower());
if (Folder != null)
{
// the Folder already exists, FileName name should be unique.
return Uow.Files.GetAll()
.Any(
x =>
x.FolderId == Folder.Id &&
x.fileName.Trim().ToLower() == fileName.Trim().ToLower()) ? Json(false, JsonRequestBehavior.AllowGet) : Json(false, JsonRequestBehavior.AllowGet);
}
// Folder name is new, in this case we can add new Folder and basked name
return Json(true, JsonRequestBehavior.AllowGet);
}
2- Create Custome remote attribute class
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// When using remote attribute, we specify the controller, action and the error message. The aim of the following is to retrieve the controller, action and error message
//using reflection.
// first get the controller
Type controller =
Assembly.GetExecutingAssembly()
.GetTypes()
.FirstOrDefault(
type =>
type.Name.ToLower() ==
string.Format("{0}Controller", this.RouteData["controller"].ToString()).ToLower());
if (controller != null)
{
// Get the method in the controller with
MethodInfo action =
controller.GetMethods()
.FirstOrDefault(method => method.Name.ToLower() == this.RouteData["action"].ToString().ToLower());
if (action != null)
{
// create instance of the controller
object instance = Activator.CreateInstance(controller);
//invoke the action method of the controller, and pass the value which is the parameter of the action
object response = action.Invoke(instance, new object[] {value});
// because the remote validation action returns JsonResult
var returnType = response as JsonResult;
if (returnType != null)
{
object jsonData = returnType.Data;
//because the jsonDate is bool
if (jsonData is bool)
{
// return success or the error message
return (bool) jsonData ? ValidationResult.Success : new ValidationResult(this.ErrorMessage);
}
}
}
}
return new ValidationResult(ErrorMessage);
}
3- My viewModel class
public class FileInViewModel
{
public string FolderName { get; set; }
[RemoteValidation("IsFileValid","Home",ErrorMessage="Please select different file name")]
public string FileName { get; set; }
// .....
}
What am i missing to make it work? If also possible to have different
error message for each error?
How can i ignore the validation for data already present when the edit page is loaded?
See the code below:
[Remote("CheckExist", "securitylevels", AdditionalFields = "Id", ErrorMessage = "the number is Zero!")]
public int Number { get; set; }
Related
I have a simple web API that performs some basic validation using validation attributes. If the request does not meet the validation requirements, I then extract the error messages from the ModelStateDictionary and return these to the caller as a dictionary, where the key is the path to the property that has an error and the message indicates what the problem is.
The problem is that, given certain input's, the error messages in the ModelStateDictionary often contains information that I would not want to return to the client (such as the full name (including namespace) of the object that the converter attempted to map the JSON to).
Is it possible to differentiate between validation errors (generated by Validation Attributes / IValidatableObject implementations) and errors generated by attempting to map invalid JSON to a certain object?
For example:
My models
public class Order
{
[Required]
public Customer Customer { get; set; }
}
public class Customer
{
[Required]
public string Name { get; set; }
}
My action method
public IActionResult Post(Order order)
{
if (!ModelState.IsValid)
{
var errors = GetErrors(ModelState);
return BadRequest(errors);
}
return Ok();
}
private Dictionary<string, string> GetErrors(ModelStateDictionary modelState)
{
var errors = new Dictionary<string, string>();
foreach (var error in modelState)
{
string message = null;
if (error.Value.Errors.Any(e => e.Exception != null))
{
message = "Unable to interpret JSON value.";
}
else
{
message = string.Join(". ", string.Join(". ", error.Value.Errors.Select(e => e.ErrorMessage)));
}
errors.Add(error.Key, message);
}
return errors;
}
My example inputs:
Missing required data
{
"Customer": {
"Name": null // name is required
}
}
Because Customer.Name is decorated with the RequiredAttribute, this generates an error message of:
{
"Customer.Name": "The Name field is required."
}
This is something that's OK to return to the caller, so no issues here.
JSON value doesnt map to .Net object type
{
"Customer": 123 // deserializing will fail to map this value to a Customer object
}
Because deserializing the value 123 to a Customer object fails, this generates an error message of:
{
"Customer": "Error converting value 123 to type 'MyProject.Models.Customer'. Path 'Customer', line 2, position 19."
}
This is not OK to return to the caller, as it contains the full namespace path to the object being mapped to. In this case, something generic (such as "Bad JSON value.") should be used as the error message.
Can anyone help me find a solution to hide these error messages which contain information that should not be returned to the caller?
As can be seen in my code above, I thought I might be able to check the ModelEntry.Exception property and use this to determine whether the error message needs to be shielded from the caller, but this is null in both my examples.
On solution may be to check if the error message starts with Error converting value, and if so, shield the message from the caller. This doesnt seem very robust, and I'm not sure how reliable this will be in a real-world example.
The default response type for HTTP 400 responses is ValidationProblemDetails class. So, we will create a custom class which inherits ValidationProblemDetails class and define our custom error messages.
public class CustomBadRequest : ValidationProblemDetails
{
public CustomBadRequest(ActionContext context)
{
ConstructErrorMessages(context);
Type = context.HttpContext.TraceIdentifier;
}
private void ConstructErrorMessages(ActionContext context)
{
//this is the error message you get...
var myerror = "Error converting value";
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(errors[0]);
if (errorMessage.StartsWith(myerror))
{
Errors.Add(key, new[] { "Cannot deserialize" });
}
else
{
Errors.Add(key, new[] { errorMessage });
}
}
else
{
var errorMessages = new string[errors.Count];
for (var i = 0; i < errors.Count; i++)
{
errorMessages[i] = GetErrorMessage(errors[i]);
if (errorMessages[i] == myerror)
{
errorMessages[i] = "Cannot deserialize";
}
}
Errors.Add(key, errorMessages);
}
}
}
}
string GetErrorMessage(ModelError error)
{
return string.IsNullOrEmpty(error.ErrorMessage) ?
"The input was not valid." :
error.ErrorMessage;
}
}
Configure in your Startup.cs:
services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomBadRequest(context);
return new BadRequestObjectResult(problems);
};
});
Be sure your Controller contains [ApiController] attribute.
Result:
Typically when model binding fails because the JSON payload is not properly formatted, then the incoming model object ("order" in your case) will be null. Just put in a simple null check and return BadRequest with a generic error message.
if (order == null)
return BadRequest("Invalid JSON");
I create a validation for type file in ASP.NET Core 3.
This is my model validation :
public class MustbeImageFile : ValidationAttribute
{
public string[] Extentions { get; set; }
public string ErrorMessage { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var file = value as IFormFile;
var extension = Path.GetExtension(file.FileName);
if (file != null)
{
if (Extentions.Equals(extension))
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
and I use it on a property :
[MustbeImageFile(ErrorMessage = "Must be Send Image File", Extentions = new string[] { "png", "jpeg", "jpg" })]
public IFormFile Photo { get; set; }
This is my controller :
public async Task<ApiReturn> RegisterUser([FromForm]RegisterDto register)
{
if (ModelState.IsValid)
{
var result = await dispatchers.SendAsync(new CreateUserCommand()
{
Email = register.Email,
UserName = register.UserName,
FirstName = register.FirstName,
LastName = register.LastName,
PhoneNumber = register.PhoneNumber
});
if (result.Success)
{
return Ok();
}
return BadRequest(result.ErrorMessage);
}
return BadRequest(ModelState);
}
I send a pdf file and I expect to get this error:
Must be image file
but I get this error instead:
Microsoft.AspNetCore.Mvc.ValidationProblemDetails
What's the problem? How can I show my error details instead of just Microsoft.AspNetCore.Mvc.ValidationProblemDetails ?
In the MustbeImageFile method, you wrote the opposite logic to determine if the current extension is included in Extentions.
Change MustbeImageFile method like this :
//...
if (!Extentions.Equals(extension))
{
return new ValidationResult(ErrorMessage);
}
//...
It's possible to define how the execution pipeline should treat BadRequest errors. For instance, you can write your own InvalidModelStateResponseFactory to specify the format of the error message. In your case, you can read the error message from your custom validation attribute.
Have a look at my answer in this post
Ideally I would like to have an URL in following format:
/api/categories/1,2,3...N/products
And this would return all products for the specified categories. Having one API call with multiple category IDs saves me several database calls, thus improves performance.
I can easily implement this in a following way.
public HttpResponseMessage GetProducts(string categoryIdsCsv)
{
// <1> Split and parse categoryIdsCsv
// <2> Get products
}
However, this doesn't look like a clean clean solution, and possibly breaking SRP principle. I also tried using ModelBinder, however it adds parameters to query string.
Questions:
Is there a clean way to implement such URL structure?
Or is there a different/better approach to retrieve all products for multiple categories?
Please let me know if you need any further clarification.
I've just found an answer to my question. Route attribute had missing parameter when using ModelBinder.
[Route("api/categories/{categoryIds}/products")]
public HttpResponseMessage GetProducts([ModelBinder(typeof(CategoryIdsModelBinder))] CategoryIds categoryIds)
{
// <2> Get products using categoryIds.Ids
}
And CategoryIds would be
public class CategoryIds
{
public List<int> Ids{ get; set; }
}
And CategoryIdsModelBinder would be
public class CategoryIdsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(CategoryIds))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
return false;
}
var values = val.AttemptedValue.Split(',');
var ids = new List<int>();
foreach (var value in values)
{
int intValue;
int.TryParse(value.Trim(), out intValue);
if (intValue > 0)
{
ids.Add(intValue);
}
}
if (ids.Count > 0)
{
var result = new CategoryIds
{
Ids= ids
};
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
We can use Post methods
[RoutePrefix ( "api/categories" )]
public class TestController
{
[HttpPost]
[Route ( "getProducts" )]
public HttpResponseMessage GetProducts ( HttpRequestMessage request )
{
HttpResponseMessage message = null;
string input = string.Empty;
input = request.Content.ReadAsStringAsync ().Result;
var ids = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>> ( input );
}
}
Unfortunately Web API can not parse your data as array or as some kind of your custom object out of the box.
If you want to parse your url param as array you can try to do:
Write your own route constraint which will read and convert your param from string to array of ints/strings/whatever;
Write your custom type converter and use it with your data model;
write your value provider and also use it with your data model
Use parameter binding
Moreover you can always use query params which is never will break principles of REST :)
Please see more details about here and here
Hope that helps
I am developing a website using asp.net mvc 4 & EF6. I want to pass a string value as a parameter in a Url.action link. However, whenever I click on the link I get this error:
The argument types 'Edm.Int32' and 'Edm.String' are incompatible for this operation. Near WHERE predicate, line 1, column 76.
This is the code that creates it:
Controller
public ActionResult Edit(string EditId)
{
if (Session["username"] != null)
{
UserInfo uinfo = db.UserInfoes.Find(EditId);
return View(uinfo);
}
else
{
return RedirectToAction("HomeIndex");
}
}
View
<a class="btn btn-info"
href="#Url.Action("Edit", "Home", new { EditId = item.regno.ToString() })"><b>Edit</b></a>
How can I use a string value as a parameter?
public ActionResult Edit(string EditId)
{
if (Session["username"] != null)
{
int id;
//Check try to parse the string into an int if it fails it will return false if it was parsed it will return true
bool result = Int32.TryParse(EditId, out id);
if (result)
{
//I wouldn't use find unless you're 100% sure that record will always be there.
//This will return null if it cannot find your userinfo with that ID
UserInfo uinfo = db.UserInfoes.FirstOrDefault(x=>x.ID == id);
//Check for null userInfo
return View(uinfo);
}
else
{
return RedirectToAction("HomeIndex");
}
}
The goal
Treat an offer as a category in controller.
The problem
I have a controller whose name is ProductsController. Inside it, I have an action called Category. When this method is requested, it responds with a view of products list that corresponds to the category passed as parameter. Follow the code:
[HttpGet]
public ActionResult Category(string categoryName = null)
{
if (Regex.Match(categoryName, #"\d+").Success)
{
int categoryId = Convert.ToInt32(Regex.Match(categoryName, #"\d+").Value);
string sluggedCategoryName = CommodityHelpers.UppercaseFirst(CommodityHelpers.GenerateSlug(Categories.GetDetails((sbyte)categoryId).Category_Name));
if (String.Format("{0}-{1}", categoryId, sluggedCategoryName) == categoryName)
{
ViewBag.Title = Categories.GetDetails((sbyte)categoryId).Category_Name;
ViewBag.CategoryProductsQuantity = Categories.GetDetails((sbyte)categoryId).Category_Products_Quantity;
ViewBag.CurrentCategory = sluggedCategoryName;
return View(Products.BuildListForHome(categoryId, null));
}
else
{
return View("404");
}
}
else
{
return View("404");
}
}
But I want to return other a specific view when "Offers" is passed as parameter.
How can I do this?
if (categoryName == "Offers")
return View("SomeView", Products.BuildListForHome(categoryId, null));
You can specify what view to return as a parameter like so:
return View("Offers", data);
Put an "if" "then" in the beginning of the method checking for Offers, and return your offers View if the conditions meet.