Use of ModelState.IsValid - c#

I've been developing with MVC for a few years now and one think I was originally taught was in a HttpPost action, the first thing you always do is perform a check on ModelState.IsValid, so...
[HttpPost]
public ActionResult Edit(ViewModel form) {
if(ModelState.IsValid) {
// Do post-processing here
}
}
I've come to an issue now where I have a hashed ID passed through the form. If it's 0, its a new record, if its above 0 it means I'm editing. Should my ModelState.IsValid ever return false, I need to setup some dropdown list data again before returning to the Edit view with the same model. To set some of these items that are returned to the form after failure, I need to know the unhashed number, but unhashing it inside the ModelState.IsValid would make it not available in the else statement.
So, is it acceptable to do the following:-
[HttpPost]
public ActionResult Edit(ViewModel form) {
int myID = 0;
if(/** unhashing is successful...**/)
{
if(ModelState.IsValid) {
// Do post-processing here
}
else
{
// Setup dropdowns using unhashed ID and return original view...
}
}
}
Note that ModelState.IsValid is not the first test inside the HttpPost. Is that acceptable? If not, is there a more appropriate way of doing such logic?
Thanks!

In your source you seem to have written some comment about unhashing but such term doesn't exist. The purpose of a hash function is to be irreversible. I think that on the other hand you have meant decrypting the query string value. Ideally this decryption should happen in a custom model binder for your view model, setting the ModelState.IsValid value to false for this parameter. So that inside your controller action all you need to check is this boolean parameter. A controller action should not be decrypting any query string or whatever parameters. This should be done much earlier in the MVC execution pipeline. A custom model binder or even a custom authorization filter would be a much better fit for this scenario.
So let's take an example:
public class ViewModel
{
public int Id { get; set; }
... some other stuff around
}
Now you could write a custom model binder for this view model:
public class MyDecryptingBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "Id")
{
var idParam = bindingContext.ValueProvider.GetValue("Id");
if (idParam != null)
{
string rawValue = idParam.RawValue as string;
int value;
if (this.DecryptId(rawValue, out value))
{
propertyDescriptor.SetValue(bindingContext.Model, value);
return;
}
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
private bool DecryptId(string raw, out int id)
{
// TODO: you know what to do here: decrypt the raw value and
// set it to the id parameter, or just return false if decryption fails
}
}
Now you can register this custom model binder with your view model at bootstrapping time (Application_Start):
ModelBinders.Binders.Add(typeof(ViewModel), new MyDecryptingBinder());
and then your controller action will be as simple as:
[HttpPost]
public ActionResult Index(ViewModel model)
{
if (ModelState.IsValid)
{
// The decryption of the id parameter has succeeded, you can use model.Id here
}
else
{
// Decryption failed or the id parameter was not provided: act accordingly
}
}

Related

Is it possible to use Web API model validation on query parameters?

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)
{
}

Is it possible to have a generic [Bind] in c# methods?

what I try to do is to bind every incomming value from my response to a string or stringlist dynamicly / generic.
So assume I would know each POST-Value of my request, e.g.
string1 = Test
string2 = Test2
I would write:
[HttpPost]
public ActionResult DoFoo(string string1, string string2)
{
}
or
[HttpPost]
public ActionResult DoFoo(string string1, [Bind(Prefix = "string2")string myString2)
{
}
My situation know is, that I have X strings with my post request. So I dont know the exact number nor the names to catch in my backend.
How to catch every given Post-value without knowing this / how to catch the values dynamicly?
I don't feel that why you have to use Prefix with BIND, when you have to bind every incoming field of response. Bind is not a good choice for that. You can use bind if you have multiple entities at the same time. Reference here
that I have X strings with my post request.
If you have to use all the fields then you can use FormCollection or Model object to receive those fields. FormCollection automatically receive all the fields from view and bind them to a collection. See this for proper example. And a code snippet is below for reference.
[HttpPost]
public ActionResult Create(FormCollection collection)
{
try
{
Student student = new Student();
student.FirstName = collection["FirstName"];
student.LastName = collection["LastName"];
DateTime suppliedDate;
DateTime.TryParse(collection["DOB"], out suppliedDate);
student.DOB = suppliedDate;
student.FathersName = collection["FathersName"];
student.MothersName = collection["MothersName"];
studentsList.Add(student);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
However if you have to deal with only one particular field/set of fields then you can use either Include or Exclude as per your convenience with BIND. Example shown here and code snipped is added below.
In following way you are telling that you only want to include "FirstName" of User model while receiving the form content. Everything else will be discarded.
[HttpPost]
public ViewResult Edit([Bind(Include = "FirstName")] User user)
{
// ...
}
And in following example you are telling that, please exclude "IsAdmin" field while receiving the fields. In this case, value of IsAdmin will be NULL, irrespective of any data entered/modified by end-user in view. However, in this way, except IsAdmin, data rest of the fields will be available with user object.
[HttpPost]
public ViewResult Edit([Bind(Exclude = "IsAdmin")] User user)
{
// ...
}

Model binder to decrypt string and convert to int

I have a model which has a property id of type int.
I pass the id in the url like Detail/20 for fetching the data. But, now my customer says they don't want to see the id, since any one can modify and see other records.
Now, I've decided to encrypt and decrypt it, and assign it to another property: encId.
public ActionResult List()
{
foreach(Employee e in empList)
{
e.encId = MyUtil.Encrypt(id,"sessionid");
}
return View(empList);
}
Finally, I make my url like Detail/WOgV16ZKsShQY4nF3REcNQ==/.
Now, all I need is to decrypt it back to the original form and assign it to the property id of type int.
public ActionResult Detail(int id) //don't want (string id)
{
}
How can I write my model binder that decrypt and convert it to valid id? Also if any error/exception occurs, it has to redirect to 404 Error page. It might happen when user manually edits some useless text in the url (encrypted id).
First, this is not the way to go about securing your website and data. Please take a look at the issues with Security Through Obscurity. You would be better off defining sets of permissions on each employee record and who can or cannot edit them. Such an example could look like this:
public ActionResult Detail(int id)
{
if(MySecurityProvider.CanView(id, HttpContext.Current.User.Identity.Name){
return View();
}
Return RedirectToAction("PermissionIssue", "Errors");
}
With that said, to continue on the path you are on, simply do the decryption within the action result.
public ActionResult Detail(string Id)
{
int actualId;
try{
actualId = MyUtil.Decrypt(id);
}catch(Exception e){
//someone mucked with my encryption string
RedirectToAction("SomeError", "Errors");
}
var employee = MyEmployeeService.GetEmployeeById(actualId);
if(employee == null){
//This was a bad id
RedirectToAction("NotFound", "Errors");
}
Return View(employee);
}

Bound object never be null

For instance, there is some object that will be model for strongly-typed view:
class SomeModel {
public string SomeUsualField { get; set; }
}
Further, some action exists in controller that will work with object specified above:
public ActionResult Index(SomeModel obj)
{
return View(obj);
}
So the question is why obj isn't a null while Index action first called? It's created new instance of SomeModel class with null SomeUsualField.
The ASP.NET MVC model binding infrastructure tries to fill all properties with data coming from the request object (query string, form fields, ...). Therefore it creates an instance of all parameters of the controller to try to match the properties.
Because you don't pass a SomeUsualField, it is null, but the parameter object has an empty instance.
You can initialize the property SomeUsualField when you call http://localhost/MySite/MyController/Index?SomeUsualField=test. The property SomeUsualField will automagically initialized with 'test'
If you don't want the parameter to be set, you can leave it away and make a 2nd action with the attribute [HttpPost]. A good tutorial is the music store.
public ActionResult Index()
{
var obj = new SomeModel();
return View(obj);
}
[HttpPost]
public ActionResult Index(SomeModel obj)
{
// update logic
return View(obj);
}

Passing list from view to controller, MVC3

I'm trying to create a tagging system for my project. I need the pass a string (for ex: "test1, test2, test3") which will be binded to an entity as a list.
I'm using EF and my view inherits an entity, defined in EF. Without creating a view model, is it possible to do that?
Quite honestly view models is the way to go here.
But because you asked I will try to answer. IIRC EF models are partial classes, meaning that you could add properties to them, like this:
public partial class MyEFModel
{
public IEnumerable<string> List
{
get
{
return SomeStringProperty.Split(',');
}
set
{
SomeStringProperty = string.Join(",", value.ToArray());
}
}
}
Another way to achieve this is to write a custom model binder, like this:
public class MyBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value != null)
{
return value.AttemptedValue.Split(',');
}
return base.BindModel(controllerContext, bindingContext);
}
}
public class HomeController : Controller
{
public ActionResult Index(
[ModelBinder(typeof(MyBinder))] IEnumerable<string> values
)
{
return View();
}
}
and then /home/index?values=val1,val2,val3 should bind correctly to the list.
There are couple of ways to achieve this:
Custom Action Filter
Custom Model Binder
These implementations can be found here:
Is it possible to change the querystring variable in ASP.NET MVC path before it hits the controller?

Categories

Resources