Why does ASP.NET Core 7 MVC ModelValidator always set ModelState.IsValid to true when the Model is invalid - asp.net-core-mvc

I'm converting an ASP.NET 4.8 MVC web site to ASP.NET Core 7 MVC. I'm running into a problem with ModelValidation where my ModelState is always valid, even when it is not. I'm using the pattern that was used in .Net 4x and worked fine. Microsoft Learn link
Controller
[HttpPost]
public async Task<IActionResult> Create(TcPhoneType tcPhoneType)
{
if (ModelState.IsValid)
{
await _phoneTypeService.CreatePhoneType(tcPhoneType);
return RedirectToAction("Index");
}
return View(tcPhoneType);
}
Model.cs
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace App.Models;
public partial class TcPhoneType
{
public string PhoneTypeId { get; set; }
public string PhoneTypeDescription { get; set; }
public virtual ICollection<MemberPhone> MemberPhones { get; } = new List<MemberPhone>();
}
ModelMetadata.cs
using System.ComponentModel.DataAnnotations;
namespace App.Models;
[MetadataType(typeof(TcPhoneTypeMetadata))]
public partial class TcPhoneType
{
}
public class TcPhoneTypeMetadata
{
[Required(AllowEmptyStrings = false, ErrorMessage = "Item is required")]
[StringLength(1)]
public string? PhoneTypeId { get; set; }
[Required(AllowEmptyStrings = false)]
[StringLength(5)]
public string? PhoneTypeDescription { get; set; }
}
Create.cshtml
#model SeasonCourt7.Models.TcPhoneType
#{
ViewBag.Title = "Add Phone Type Code";
}
<h2>#(ViewBag.Title)</h2>
<br>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row mb-3">
<div class="col-sm-2">
<label asp-for="PhoneTypeId" class="col-form-label">Code</label>
</div>
<div class="col-sm-10">
<input asp-for="PhoneTypeId" class="form-control" autofocus />
</div>
<span asp-validation-for="PhoneTypeId" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-sm-2">
<label asp-for="PhoneTypeDescription" class="col-form-label">Description</label>
</div>
<div class="col-sm-10">
<input asp-for="PhoneTypeDescription" class="form-control" />
</div>
<span asp-validation-for="PhoneTypeDescription" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="offset-sm-2 col-sm-10">
<input type="submit" value="Create" class="btn btn-primary" />
Back
</div>
</div>
</div>
}
#section Scripts {
<partial name="_ValidationScriptsPartial" />
}
I set a breakpoint on the if (ModelState.IsValid) line so I could inspect the model and ModelState. I can see the data that is bound to the model is as expected and the ModelState is always valid.
If I pass null data in the model, I should see the ModelState.IsValid = false, but it's always true.
The model.cs is generated by EF and could be regenerated in the future which makes it impractical to add the metadata attributes for validation, as the file could be regenerated in the future.
The only way I've been able to get the ModelValidation to work as expected, is to decorate the model directly with the validation attributes.

Related

Edit method in ASP.NET Core 6 MVC

I am building an e-store web app using ASP.NET Core 6 MVC. I am trying to do CRUD operations with the help of some tutorial, everything went smoothly, but when I try to edit a product, it makes a copy of that edited item, instead of just replacing it.
Also, it asks me to upload a new image even though I want to set it not to ask for a new one, and there is a default (noimage.png) set if there is no image uploaded. Here is the edit method, please tell me where am going wrong.
public async Task<IActionResult> Edit(int id, Product product)
{
ViewBag.CategoryId = new SelectList(_context.Category.OrderBy(x => x.Sorting),
"Id", "Name", product.CategoryId);
// if (ModelState.IsValid)
// {
product.Slug = product.Name.ToLower().Replace(" ", "-");
var slug = await _context.Product
.Where(x => x.Id != id)
.FirstOrDefaultAsync(x => x.Slug == product.Slug);
if (slug != null)
{
ModelState.AddModelError("", "The product already exists.");
return View(product);
}
// Not mandatory to upload an image when editing
if (product.ImageUpload != null)
{
string uploadsDir = Path.Combine(webHost.WebRootPath, "media/products");
// If the image is not noimage.png then remove the existing image and upload a new one
if (!string.Equals(product.Image, "noimage.png"))
{
string oldImagePath = Path.Combine(uploadsDir, product.Image);
if (System.IO.File.Exists(oldImagePath))
{
System.IO.File.Delete(oldImagePath);
}
}
// Upload new image
string imageName = Guid.NewGuid().ToString() + "_" + product.ImageUpload.FileName;
string filePath = Path.Combine(uploadsDir, imageName);
FileStream fs = new FileStream(filePath, FileMode.Create);
await product.ImageUpload.CopyToAsync(fs);
fs.Close();
product.Image = imageName;
}
_context.Update(product);
await _context.SaveChangesAsync();
TempData["Success"] = "The product has been edited!";
return RedirectToAction("Index");
// }
// return View(product);
}
Product class
public class Product
{
public int Id { get; set; }
[Required, MinLength(2, ErrorMessage = "Minimum length 2")]
public string? Name { get; set; }
public string? Slug { get; set; }
[Column(TypeName = "decimal (18,2)")]
public decimal Price { get; set; }
[Display(Name = "Category")]
[Range(1, int.MaxValue, ErrorMessage = "You must choose a category")] //specific validation for category
public int? CategoryId { get; set; }
[Required, MinLength(2, ErrorMessage = "Minimum length 4")]
public string? Description { get; set; }
public string? Image { get; set; }
//To make the connection
[ForeignKey("CategoryId")]
public virtual Category? Category { get; set; }
[NotMapped] //to not show in the DB
[ImageExtention]
public IFormFile? ImageUpload { get; set; }
}
Edit view
#model Product
#{
ViewData["Title"] = "Edit Product";
}
<h1>Edit Product</h1>
<div class="row">
<div class="col-md-10">
<form asp-action="Edit" enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
#*<input type="hidden" asp-for="Image" />*#
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CategoryId" class="control-label"></label>
<select asp-for="CategoryId" asp-items="#ViewBag.CategoryId" class="form-control">
<option value="0">Choose a category</option>
</select>
<span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Image" class="control-label">Current Image</label>
<img src="~/media/products/#Model.Image" width="200" alt="" />
</div>
<div class="form-group">
<label asp-for="Image" class="control-label">New Product Image</label>
<input asp-for="ImageUpload" class="form-control" />
<img src="" id="imgpreview" class="pt-2" alt="" />
<span asp-validation-for="ImageUpload" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Edit" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
$("#ImageUpload").change(function () {
readURL(this);
});
</script>
}
You need to make Id as Primary Key in Product
public class Product
{
[Key]
public int Id { get; set; }
//ommitted
}
You are not posting Id to Edit Method while Edit button clicked. So every time It is sending value of Id as 0. EntityFramework is considering it as new object.
Please add below code to cshtml view inside form. It will send correct value of existing product Id.
<input type="hidden" asp-for="Id" />

Input field keeps switching between text and date in ASP.NET Core MVC application

I'm having a problem with an input field switching between type=text (when it's not focused) and type=date (when it's focused). I'm using .NET Core 3.1 framework.
Model:
public class DocumentVersionModel
{
public int Id { get; set; }
public int DocumentModelId { get; set; }
[DataType(DataType.Date, ErrorMessage="Date only")]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime CreationDate { get; set; }
[DataType(DataType.Date, ErrorMessage="Date only")]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime LastChangeDate { get; set; }
[DataType(DataType.Date, ErrorMessage="Date only")]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime DocumentDate { get; set; }
public string Content { get; set; }
public int Version { get; set; }
public DocumentModel Document { get; set; }
}
The format yyyy-MM-dd is needed for the database and is the one displayed when the input field isn't focused. The format I need to display the dates is mm/dd/yyyy, but I can't change the DateFormatString (and removing ApplyFormatInEditMode = true didn't work).
This is the form in the RazorPage:
<form asp-action="Edit">
<br />
<div class="row justify-content-center">
<div class="col-12 form-group">
<label>Titolo</label>
<input asp-for="Title" class="form-control" />
</div>
<div class="col">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" id="iddoc" />
<div class="form-group">
<label>Data Documento</label>
<input asp-for="DocumentVersion.FirstOrDefault(q => q.Version==Model.ActiveVersion).DocumentDate" class="form-control" />
<span asp-validation-for="DocumentVersion.FirstOrDefault(q => q.Version==Model.ActiveVersion).DocumentDate" class="text-danger"></span>
</div>
<div class="form-group">
<label>Data Creazione</label>
<input asp-for="DocumentVersion.FirstOrDefault(q => q.Version==Model.ActiveVersion).CreationDate" class="form-control" readonly="readonly" />
</div>
<div class="form-group">
<label>Data Ultima Modifica</label>
<input asp-for="DocumentVersion.FirstOrDefault(q => q.Version==Model.ActiveVersion).LastChangeDate" class="form-control" readonly="readonly" />
</div>
</div>
</form>
A little note about the form: if I use type="date" or "datetime" inside the input tag, the type keeps changing between text and date, but if I use "datetime-local" is shown correctly (but it displays the time too which I don't need).
This is the GIF of what happens:
https://imgur.com/YXKVAMO
This is the very bad and inefficient jQuery solution I used to "fix" the problem:
$('#DocumentDate, #LastChangeDate, #CreationDate').attr('type','date'); //when the page loads
$(document).click(function () {
if ($('#DocumentDate').not(':focus') || $('#LastChangeDate').not(':focus') || $('#CreationDate').not(':focus'))
$('#DocumentDate, #LastChangeDate, #CreationDate').attr('type','date');
});
So what is your current problem? Do you not want to use jquery to
achieve it?
I think the method you are using by jquery is implemented very well because you cannot change the date type input control to display the date format you want: link.
Another solution is to use Datepicker plugin for jquery UI, but this cannot avoid using jquery.
Here is the example based jquery UI Datepicker :
#model WebApplication_core_mvc.Models.DocumentVersionModel
#{
ViewData["Title"] = "DateEdit";
}
<h1>DateEdit</h1>
#section Scripts{
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$(function () { // will trigger when the document is ready
$('.datepicker').datepicker();
$('.datepicker').datepicker("option", "dateFormat", "mm/dd/yy"); //Initialise any date pickers
});
</script>
}
<form asp-action="Edit">
<br />
<div class="row justify-content-center">
<div class="col-12 form-group">
<label>Titolo</label>
<input asp-for="DocumentModelId" class="form-control" />
</div>
<div class="col">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" id="iddoc" />
<div class="form-group">
<label>Data Documento</label>
#Html.TextBoxFor(model => model.CreationDate, new { #class = "form-control datepicker" })
<span asp-validation-for="CreationDate" class="text-danger"></span>
</div>
<div class="form-group">
<label>Data Creazione</label>
#Html.TextBoxFor(model => model.LastChangeDate, new { #class = "form-control datepicker" })
</div>
<div class="form-group">
<label>Data Ultima Modifica</label>
#Html.TextBoxFor(model => model.DocumentDate, new { #class = "form-control datepicker" })
</div>
</div>
</div>
</form>
Here is the result:

Server side validation problem - float value

I have a problem with data validation in my application.
Im using a Razor Pages and .Net Core 3.0 framework with EF Core orm.
In my model I have two properties:
public float WireCrosssection { get; set; }
public float CableLength { get; set; }
On page, I have inputs for them:
<div class="form-group">
<label asp-for="Cable.WireCrosssection"></label>
<input class="form-control" asp-for="Cable.WireCrosssection" />
<span class="text-danger" asp-validation-for="Cable.WireCrosssection"></span>
</div>
<div class="form-group">
<label asp-for="Cable.CableLength"></label>
<input class="form-control" asp-for="Cable.CableLength" />
<span class="text-danger" asp-validation-for="Cable.CableLength"></span>
</div>
Client side validation is turned on and this validation doesn't report problems with the form but the server side one do (ModelState.IsValid is false).
The number is provided with dot (".").
Any suggestions?
Be sure you have added the validation script in your view.
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
Here is a simple demo like below:
1.Model:
public class Test
{
public float WireCrosssection { get; set; }
public float CableLength { get; set; }
}
2.View:
#model Test
<form asp-action="Index">
<div class="form-group">
<label asp-for="WireCrosssection"></label>
<input class="form-control" asp-for="WireCrosssection" />
<span class="text-danger" asp-validation-for="WireCrosssection"></span>
</div>
<div class="form-group">
<label asp-for="CableLength"></label>
<input class="form-control" asp-for="CableLength" />
<span class="text-danger" asp-validation-for="CableLength"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
3.Controller:
[HttpPost]
public IActionResult Index(Test test)
{
//..
}
4.Result:
Reference:Client Side Validation
Ok, I solved the issue. The problem was a mismatch in culture on the server side and on the client side. Client side validation is written in "en-US" culture. To set the culture on the ASP.NET Core application you need to add following code to the Configure method in the Startup.cs class:
var cultureInfo = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;

Remote validation fails with IFormFile attribute

I notice that remote validation in ASP.NET core always fails because the server always receives a null IFormFile in the controller method. Is there a way to make it work?. Some code to reproduce the problem:
The model class ("not mapped" was included so Entity Framework doesn't interfere, but it also didn't work in another project without Entity Framework.
public class Movie
{
public int ID { get; set; }
[Sacred(sacredWord: "sonda")]
public string Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[Remote(action: "VerifyRating", controller: "Movies")]
public string Rating { get; set; }
[NotMapped]
[Remote(action: "VerifyFile", controller: "Movies"),Required]
public IFormFile File { get; set; }
}
The controller
public class MoviesController : Controller
{
private readonly WebAppMVCContext _context;
public MoviesController(WebAppMVCContext context)
{
_context = context;
}
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
[AcceptVerbs("Get", "Post")]
public IActionResult VerifyFile(IFormFile File)
{
if(File == null)
{
return Json("The file is null");
}
else
{
return Json("The file is not null");
}
}
// POST: Movies/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(movie);
}
[AcceptVerbs("Get", "Post")]
public IActionResult VerifyRating( int rating)
{
if(rating>0 && rating < 10)
{
return Json(true);
}
else
{
return Json($"The rating is invalid");
}
}
and the View
#model WebAppMVC.Models.Movie
#{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Rating" class="control-label"></label>
<input asp-for="Rating" class="form-control" />
<span asp-validation-for="Rating" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="File" class="control-label"></label>
<input asp-for="File" />
<span asp-validation-for="File" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts{
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$.validator.addMethod('sacred',
function (value, element, params) {
var title = $(params[0]).val(),
sacredword = params[1];
if (title!=null && title == sacredword) {
return true;
}
else {
return false;
}
}
);
$.validator.unobtrusive.adapters.add('sacred',
['sacredword'],
function (options) {
var element = $(options.form).find('input#Title')[0];
options.rules['sacred'] = [element, options.params['sacredword']];
options.messages['sacred'] = options.message;
}
);
</script>
}
Notice that all the other validations work (including the remote validation "VerifyRating").

How to preserve current view after api call

Before asp.net core I would invoke a web api from a button by writing some jquery from behind the button click.
I would then handle the data returned from that api call.
Currently I have a view. this view contains a pop up window. In that window I have 3 div sections.
Login
Registration
ForgottenPassword
Focusing on Registration div I allow user to enter email/password/confirm password and let them press a button that calls my web api to validate this process.
So, this is my parent view snippet:
#model InformedWorker.Services.Models.Account
<div id="divLogForm" class="modal-dialog" style="display:none;position:absolute; ">
<div style="background-color: #fefefe;margin: auto;padding: 20px;border: 1px solid #888;">
<div class="modal-header">
<button id="btnClose" type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<h4 class="modal-title" id="hdrTitle">LogIn</h4>
</div>
<div class="modal-body">
#{Html.RenderPartial("~/Views/Segments/_Registration.cshtml", Model.Registration);}
#{Html.RenderPartial("~/Views/Segments/_LogIn.cshtml", Model.LogIn);}
#{Html.RenderPartial("~/Views/Segments/_ForgottenRegistration.cshtml", Model.ForgottenloginDetails);}
</div>
</div>
</div>
My registration partial view is this:
#model InformedWorker.Services.Models.Registration
#*
*#
<div class="row" style="display:none" id="divRegistration">
<div class="col-xs-6">
<div class="well">
<div class="form-group">
<label asp-for="EmailAddress" class="control-label"></label>
<input type="text" class="form-control" id="EmailAddress" name="EmailAddress" value="#Model.EmailAddress" required="" title="Please enter you username" placeholder="example#gmail.com">
<span class="help-block"></span>
</div>
<div class="form-group">
<label asp-for="Password" class="control-label">Password</label>
<input type="password" class="form-control" id="Password" name="Password" value="" required="" title="Please enter your password">
<span class="help-block"></span>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="control-label"></label>
<input type="password" class="form-control" id="ConfirmPassword" name="ConfirmPassword" value="" required="" title="Please confirm your password">
</div>
<div id="registerErrorMsg" class="alert alert-error hide">Invalid registration</div>
<a asp-area="" asp-controller="api/Registration" asp-action="Register" class="btn btn-success btn-block">Register</a>
</div>
</div>
<div class="col-xs-6">
<p class="lead">Already Registered?</p>
<p><a onclick="showLogInPage();" class="btn btn-info btn-block">LogIn</a></p>
</div>
</div>
My account Model is this:
public class Account
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long AccountId { get; set; }
public string AccountRef { get; set; }
[Display(Name = "Company Name")]
public string CompanyName { get; set; }
[Display(Name = "Email Address")]
[Required]
[RegularExpression(#"\w+([-+.']\w+)*#\w+([-.]\w+)*\.\w+([-.]\w+)*", ErrorMessage = "Email address not found")]
[StringLength(60, MinimumLength = 3)]
public string EmailAddress { get; set; }
public string Salt { get; set; }
public string Hash { get; set; }
public bool Disabled { get; set; }
[Display(Name = "Date of Entry")]
public DateTime DOE { get; set; }
public ICollection<Client> Clients { get; set; }
public bool Activated { get; set; }
[NotMapped]
public Registration Registration { get; set; }
[NotMapped]
public LogIn LogIn { get; set; }
[NotMapped]
public ForgottenRegistration ForgottenRegistration { get; set; }
}
My homeController is this:
public IActionResult Index()
{
//var home = new Services.Models.Home();
//home.MyTest = _localizer["ActivationEmailSent"];
//return View(home);
var test = new Services.Models.Account();
test.Registration = new Services.Models.Registration();
test.Registration.EmailAddress = "aaa";
return View(test);
//return View(new Services.Models.Account());
}
My Registration Web api is this:
[Route("api/[controller]")]
public class Registration : Controller
{
// GET api/values/5
[HttpGet("{id}")]
public string Get(Registration Account)//int id)
{
return "Error in reg";
}
}
so my question is that when api is called the whole page page is replaced with 'value'.
how would I parse the data and populate the form fields with the result and preserve the original view?
Should i revert back to jquery and make api call that way?
Have I got this fundamentally wrong?
Thanks

Resources