I'm using aspnetboilerplate framework in a .Net Core project. I have a custom validation for my view model following on official documentation.
First I have a simple view model with custom validation:
public class EditPropertyViewModel : ICustomValidate
{
public long Id { get; set; }
public long? ParentId { get; set; }
public string Title { get; set; }
public void AddValidationErrors(CustomValidationContext context)
{
if (Id == ParentId)
context.Results.Add(new ValidationResult("Property cannot be parent of itself!", new [] { "ParentId" } ));
}
}
Then my controller is like this:
[HttpPost]
public async Task<IActionResult> Edit(EditPropertyViewModel model)
{
if (ModelState.IsValid)
{
/* Update property here and return */
}
return View(model);
}
But when I run the project, this exception occures:
AbpValidationException: Method arguments are not valid! See ValidationErrors for details.
That means my custom validation has been executed before ModelState.IsValid and there is no chance to handle that exception and show a user friendly message to the user. Disabling validation by [DisableValidation] skips this exception but my validation logic is skipped too. I also tryed to use .NET's standard IValidatableObject interface instead of the abp's ICustomValidate but this not helped me to solve the problem.
Related
I'm trying to write a custom validation class for an ASP.NET Core web app I'm developing. I've found various examples of how to write custom client-side validation, such as this and this. These are all clear and make sense to me. However, my problem is that my model is defined within a .NET Standard library that other projects share. I cannot access the base classes I need to create the custom validation class from within this library.
Basically, I need to ensure that model.PropertyA is never greater than model.PropertyB. I'm aware that I could write some JavaScript that accomplishes this, but I'd prefer to utilize the existing ASP.NET Core validation techniques if possible.
I'd prefer to avoid any 3rd party dependencies to accomplish this, if possible.
hy,
Better to avoid Validation against Data Annotation because you don't have access to them in all cases , like the case you are describing;
One powerful package exist "FluentValidation "
you can create a model class and validate against properties
sample:
public class Person {
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
full documentation : https://docs.fluentvalidation.net/en/latest/aspnet.html
and then you add a validator for your commands like the following sample :
public class CreatePersonCommandValidator :
AbstractValidator<CreatePersonCommand>
{
....
public CreateTodoListCommandValidator(IApplicationDbContext context)
{
_context = context;
RuleFor(v => v.Name)
.NotEmpty().WithMessage("Name is required.")
.MaximumLength(200).WithMessage("Name must not exceed 200 characters.");
}...
....
..
You could build a class extends the class from libraries. And extends IValidatableObject to implement Validation.
Base class is from librarie.
public class Base
{
public int PropertyA { get; set; }
public int PropertyB { get; set; }
}
MyClass is build for validation.
public class MyClass : Base, IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (PropertyA > PropertyB)
{
yield return new ValidationResult(
$"model.PropertyA {PropertyA} can't greater than model.PropertyB ." );
}
}
}
Test codes:
[HttpPost]
public IActionResult Add([FromBody]MyClass myClass)
{
if (ModelState.IsValid)
{
RedirectToAction("Index");
}
return View();
}
Test result:
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
I'm developing an Asp.Net Core 2.1 App using Razor Pages.I've come down with an odd behavior.The problem is when I submit a form,the client side validation passes with all required properties filled out,but then the validation fails with the ModelState.IsValid check,and the reason is that the ModelState contains the required string properties twice,one with the value entered and one with null value,So the validation fails!
{[BankName, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary+ModelStateNode]}
{[BankAccount.BankName, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary+ModelStateNode]}
See,BankAccount is the model class,and BankName is a required property.I don't know why the property appears twice in ModelState dictionary,one with the model name(with the data entered) and one without the modelname(with null value)
Any idea why this is happening?
public class BankAccount
{
[DisplayName("")]
public int BankAccountId { get; set; }
[MaxLength(20, ErrorMessage = "")]
[Required(ErrorMessage = "")]
[DisplayName("")]
public string BankName { get; set; }
...
Here' the code OnPost() where the validation fails:
public async Task<IActionResult> OnPostAsync()
{
// TODO: Not ideal! But solves the problem of returning invalid model state.
ModelState.Remove("BankName");
if (!ModelState.IsValid)
{
return RedirectToPage();
}
_context.BankAccounts.Add(BankAccount);
await _context.SaveChangesAsync();
return RedirectToPage();
}
After searching a lot,I found a workaround,which isn't very ideal.That's to remove the additional property that has oddly been inserted in ModelState dictionary. I mean this line:
ModelState.Remove("BankName");
But that's not the right way.I'd like to figure out why it's happening?!
Here are two properties defined on the PageModel:
[BindProperty]
public BankAccount BankAccount { get; set; }
[BindProperty]
public BankAccount BankAccountEdit { get; set; }
One is used to insert new BankAccount and the other one is used to edit existing ones by clicking on a button from the table.
I figured out the the issue.The problem was that I have two properties of the same type(BankAccount class) in my page model,one for inserting new entity and the other one for editing existing entity all on the same page.
So to validate each form separately OnPost(),I used the following code:
public async Task<IActionResult> OnPostAsync()
{
var validateBankAccount = ModelState.GetFieldValidationState("BankAccount");
if (validateBankAccount ==
Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
return RedirectToPage();
}
_context.BankAccounts.Add(BankAccount);
await _context.SaveChangesAsync();
return RedirectToPage();
}
I am currently trying to write a Web API application where one of the parameters I'd like to validate is a query parameter (that is, I wish to pass it in in the form /route?offset=0&limit=100):
[HttpGet]
public async Task<HttpResponseMessage> GetItems(
int offset = 0,
int limit = 100)
{
if (!ModelState.IsValid)
{
// Handle error
}
// Handle request
}
In particular, I want to ensure that "offset" is greater than 0, since a negative number will cause the database to throw an exception.
I went straight for the logical approach of attaching a ValidationAttribute to it:
[HttpGet]
public async Task<HttpResponseMessage> GetItems(
[Range(0, int.MaxValue)] int offset = 0,
int limit = 100)
{
if (!ModelState.IsValid)
{
// Handle error
}
// Handle request
}
This does not cause any errors at all.
After a lot of painful debugging into ASP.NET, it appears to me that this may be simply impossible. In particular, because the offset parameter is a method parameter rather than a field, the ModelMetadata is created using GetMetadataForType rather than GetMetadataForProperty, which means that the PropertyName will be null. In turn, this means that AssociatedValidatorProvider calls GetValidatorsForType, which uses an empty list of attributes even though the parameter had attributes on it.
I don't even see a way to write a custom ModelValidatorProvider in such a way as to get at that information, because the information that this was a function parameter seems to have been lost long ago. One way to do that might be to derive from the ModelMetadata class and use a custom ModelMetadataProvider as well but there's basically no documentation for any of this code so it would be a crapshoot that it actually works correctly, and I'd have to duplicate all of the DataAnnotationsModelValidatorProvider logic.
Can someone prove me wrong? Can someone show me how to get validation to work on a parameter, similar to how the BindAttribute works in MVC? Or is there an alternative way to bind query parameters that will allow the validation to work correctly?
You can create a view request model class with those 2 properties and apply your validation attributes on the properties.
public class Req
{
[Range(1, Int32.MaxValue, ErrorMessage = "Enter number greater than 1 ")]
public int Offset { set; get; }
public int Limit { set; get; }
}
And in your method, use this as the parameter
public HttpResponseMessage Post(Req model)
{
if (!ModelState.IsValid)
{
// to do :return something. May be the validation errors?
var errors = new List<string>();
foreach (var modelStateVal in ModelState.Values.Select(d => d.Errors))
{
errors.AddRange(modelStateVal.Select(error => error.ErrorMessage));
}
return Request.CreateResponse(HttpStatusCode.OK, new { Status = "Error",
Errors = errors });
}
// Model validation passed. Use model.Offset and Model.Limit as needed
return Request.CreateResponse(HttpStatusCode.OK);
}
When a request comes, the default model binder will map the request params(limit and offset, assuming they are part of the request) to an object of Req class and you will be able to call ModelState.IsValid method.
For .Net 5.0 and validating query parameters:
using System.ComponentModel.DataAnnotations;
namespace XXApi.Models
{
public class LoginModel
{
[Required]
public string username { get; set; }
public string password { get; set; }
}
}
namespace XXApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
[HttpGet]
public ActionResult login([FromQuery] LoginModel model)
{
//.Net automatically validates model from the URL string
//and gets here after validation succeeded
}
}
}
if (Offset < 1)
ModelState.AddModelError(string.Empty, "Enter number greater than 1");
if (ModelState.IsValid)
{
}
I have a logging controller in my project that gets Log data from javascript client side and sends to server.
Log Model ils like this.
public class Log
{
public string Event { get; set; }
public string Message { get; set; }
public DateTime Date { get; set; }
public string Username { get; set; }
}
My controller action is like this.
[HttpPost]
public JsonResult Save(Log log)
{
log.Username = User.Identity.Name;
log.Date = DateTime.now;
return Json(new { message = "ok" }, JsonRequestBehavior.AllowGet);
}
when users sended the log data, I need to set the username and date. Is there another way with model binding or else? Automatically set the context.username and date time of log.
IModelBinder is the preferred way to go. You implement the interface and register it in Global.asax file
ModelBinders.Binders.Add(typeof(Log), new LogModelBinder());
What I don't like about this approach is that you have to be aware that there is a custom model binder for this type implemented somewhere and it can cause confusion if you forget about it, or you have a new developer working on the project and model starts behaving strange. It is just not obvious enough what is changing the object.
I like to have all my custom logic implemented in the controller itself. You can override OnActionExecuting and inject the properties there, better yet, have a BaseController that all your controllers inherit from and do your custom logic in there.
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
object model = filterContext.ActionParameters
.TryGetValue("model", out model)
? model : null;
if (model != null)
{
var logModel = model as Log;
if (logModel != null)
{
logModel.Date = DateTime.Now;
logModel.Username = filterContext.HttpContext.User.Identity.Name;
}
}
}
I'm trying to add a form to allow users to comment on posts on my blogging application. So far, I've added a form to the post details view and I can submit comments, adding them to my database correctly. However, I have a problem with displaying validation errors to the user. The comment form is contained within a partial view and is rendered using Html.RenderAction inside the post details view. I'd like to stress that I don't want to use AJAX for this as I'd like to approach this from a progressive enhancement point-of-view.
Here's the relevant posting action:
[HttpPost, Authorize]
public ActionResult AddComment(CommentViewModel newComment)
{
if (ModelState.IsValid)
{
Comment comment = new Comment(_userRepository.GetByUsername(User.Identity.Name));
Mapper.Map(newComment, comment);
_commentRepository.Add(comment);
_postsRepository.CommentAdded(comment.Article);
return RedirectToAction("Index", new { id = newComment.PostID });
}
// What do I do here?
}
I've tried several ways of returning views here but my issue is further complicated by some controller parameter validation that I have going on in the parent action:
//
// GET: /Posts/5/this-is-a-slug
public ActionResult Index(int id, string slug)
{
PostViewModel viewModel = new PostViewModel();
var model = _postsRepository.GetByID(id);
if (model != null)
{
if (slug == null || slug.CompareTo(model.Slug) != 0)
{
return RedirectToActionPermanent("Index", new { id, slug = model.Slug });
}
else
{
_postsRepository.PostVisited(model);
Mapper.Map(model, viewModel);
viewModel.AuthorName = _userRepository.GetById(model.AuthorID);
}
}
return View(viewModel);
}
This action basically mimics how SO's URLs work. If a post ID is supplied, the post is fetched from the database along with a slug which is created when the post is created. If the slug in the URL doesn't match the one in the database, it redirects to include the slug. This is working nicely but it does mean I'm having issues trying to populate my parent viewmodel, which is the following:
public class PostViewModel
{
public int PostID { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string Slug { get; set; }
public DateTime DatePublished { get; set; }
public int NumberOfComments { get; set; }
public int AuthorID { get; set; }
public string AuthorName { get; set; }
public List<CommentViewModel> Comments { get; set; }
public CommentViewModel NewComment { get; set; }
}
What I was hoping would work is to populate PostViewModel.NewComment, test to see if it has data and then using it to display any model errors. Unfortunately, I'm lost as to how to accomplish that. This question helped me shape my approach, but it didn't quite answer my problem.
Could someone give me a gentle push in the right direction? If my approach seems unreasonable, I'd love to find out why and what a potential fix would be.
Many thanks in advance.
Forgot to fill in my answer here. For anyone that happens to stumble on this, the answer was to use TempData to store the ModelState errors and then repopulating ModelState in the relevant controller action.
Firstly, I declared a key in the controller which would be used to reference the data inside TempData. I decided to base this on the CommentViewModel type as both actions depend on it.
public class PostsController : Controller
{
private static readonly string commentFormModelStateKey = typeof(CommentViewModel).FullName;
// Rest of class.
}
In this first action, the code checks to see if TempData contains data assigned to the key. If it does, it's copied into ModelState.
// GET: /posts/comment
[ChildActionOnly]
public PartialViewResult Comment(PostViewModel viewModel)
{
viewModel.NewComment = new CommentViewModel(viewModel.PostID, viewModel.Slug);
if (TempData.ContainsKey(commentFormModelStateKey))
{
ModelStateDictionary commentModelState = TempData[commentFormModelStateKey] as ModelStateDictionary;
foreach (KeyValuePair<string, ModelState> valuePair in commentModelState)
ModelState.Add(valuePair.Key, valuePair.Value);
}
return PartialView(viewModel.NewComment);
}
This action determines if the ModelState is valid before adding a comment to the database. If the ModelState is not valid, it is copied to TempData, which makes it available to the first action.
// POST: /posts/comment
[HttpPost, Authorize]
public ActionResult Comment(CommentViewModel newComment)
{
if (!ModelState.IsValid)
{
TempData.Add(commentFormModelStateKey, ModelState);
return Redirect(Url.ShowPost(newComment.PostID, newComment.Slug));
}
// Code to add a comment goes here.
}