This is the spiritual successor to my previous question Web API attribute routing and validation - possible?, which I think was too general to answer. Most of those issues are solved, but the default value question remains.
Basically I have solved many pieces of the puzzle. I have this:
[HttpGet]
[Route("test/{id}"]
public IHttpActionResult RunTest([FromUri]TestRequest request)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
return Ok();
}
My TestRequest class:
public class TestRequest
{
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; }
}
The problem is that if no parameter is in the query string for something, the model is "valid" and yet something is null.
If I specify a blank value for something (i.e. GET test/123?something=), then the default value comes into play, and the model is valid again.
Why is this? How can I get a default value into my model here? As a bonus, why is it when a parameter is not specified, the default value is not used, but when a blank string is explicitly specific, the default value is used?
(I've been trawling through the ASP.NET stack source code and am knee-deep in model binders and binding contexts. But my best guess can't be right - it looks like the DefaultValueAttribute is used only if the parameter value is null. But that's not the case here)
You need to initialize the default value in the constructor for your Model:
public class TestRequest
{
public TestRequest()
{
this.something = "SomethingDefault";
}
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; }
}
Update:
With C# 6, you don't need to initialize it in the constructor anymore. You can assign the default value to the property directly:
public class TestRequest
{
public string id { get; set; }
[DefaultValue("SomethingDefault")]
public string something { get; set; } = "SomethingDefault";
}
As documentation of the DefaultValueAttribute states:
Note
A DefaultValueAttribute will not cause a member to be
automatically initialized with the attribute's value. You must set the
initial value in your code.
In the case where you're providing no value for your something property, the property is initialized and the ModelBinder doesn't have a value to assign to it and thus the property defaults to its default value.
Specifying the default in the constructor works for when no parameter is specified at all, but when a blank string is specified, null is put into the field instead.
As such, adding [DefaultValue("")] actually worked the best - when a blank string was specified, a blank string was passed in. Then the constructor can specify default values for when the parameter is missing.
To get around this, I've created PreserveBlankStringAttribute, derives from DefaultValueAttribute which is equivalent to [DefaultValue("")].
I would very much welcome a better answer than this, please.
Related
I'm trying to get some data from the request body in a POST Controller, but the console shows empty props:
The Post Controller:
[HttpPost("{id}/features")]
public ActionResult<bool> AddFeatureAsync(Guid Id, [FromBody] AddRoleFeatureRequest request)
{
Console.WriteLine(request.Name);
Console.WriteLine(request.Description);
Console.WriteLine(request.Id);
return true;
}
The AddRoleFeatureRequest class:
public class AddRoleFeatureRequest
{
public Guid Id;
public string? Name;
public string? Description;
}
The JSON data from Postman (Using body raw as Json):
{
"name": "Feature ABC",
"description": "description",
"id": "7e12b0ad-2c82-46f0-a69e-8538efb0aa60"
}
What am I doing wrong?
I'm trying to get some data from the request body in a POST
Controller, but the console shows empty props:
Your reason for getting null data on your console or in controller is pretty obvious because you have defined your AddRoleFeatureRequest class field only which doesn't allow to set any value on it. For instance, public string? Name; is a field not property. To set value, you must implement valid setter. Thus, it can be treated as valid property and able to assign value into it.
Solution:
public class AddRoleFeatureRequest
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
}
Note: Property without getter and setter will always consider as field, it will not allow you to assign value from outside.
Output:
Note: Modifying your class defination would completely resolve your issue. No other changes required.
Your "AddRoleFeatureRequest" class has capitals and your json data does not. This could be the source of your problems.
the attribute names might be the reasons because they ar different then the json keys, retry it while considering letters cases,
I'm currently working on a content editor that can be used for multiple types of content, where a developer could specify their own model. For example, a model might look like this:
public class ImageWithCopyWidgetModel : WidgetModel, IWidgetModel
{
public string ImageUrl { get; set; }
public string ImageAltText { get; set; }
public string HeaderText { get; set; }
public string BodyContent { get; set; }
}
On the editor side, I have a view model that looks like:
public class EditContentViewModel<TModel> where TModel : IWidgetModel
{
public int Id { get; set; }
public string Name { get; set; }
public TModel WidgetModel { get; set; }
}
I have the binding on the GET/form display side working fine. My issue comes with getting the model binder to accept the data on the POST? I've tried the following, but each returns null for model.WidgetModel:
// Option 1
EditContent(int pageId, int id, EditContentViewModel<dynamic> model)
// Option 2
EditContent(int pageId, int id, EditContentViewModel<object> model)
// Option 3
EditContent(int pageId, int id, EditContentViewModel<IWidgetModel> model)
Note, for testing purposes, I tried explicitly setting the type of WidgetModel to a concrete class (the ImageWithCopyWidgetModel noted above) and that works.
I'm really trying to avoid having to use Request.Form here as its going to limit future plans for this implementation.
What you're wanting is not possible, at least out of the box. On post, all the modelbinder has is a bunch of key-value pair string. What informs its decision about how to bind those values to something useful is the action param(s). Specifically, it has no way of knowing that it should actually create an instance of ImageWithCopyWidgetModel when you're binding to EditContentViewModel.
Also, the modelbinder is designed to discard values it doesn't know what to do with. That means that it's unfortunately not even possible to cast to ImageWithCopyWidgetModel after the fact, because all properties not present on EditCopyViewModel would have been discarded by that point.
Your best bet is a custom model binder, but the implementation of that is too broad for the scope of Stack Overflow. I suggest you refer to the documentation.
We are creating RestService with Asp.Net WebApi. But for some reason Name property is ignored in DataMember attribute when trying to deserialize complex property with [FromURI] attribute.
For example we might have:
Method:
public IHttpActionResult Get([FromUri]User user)
Model:
[DataContract]
public class User
{
[DataMember(Name = "username")]
public string Username{ get; set; }
[DataMember(Name = "isActive", IsRequired = false)]
public bool? Active { get; set; }
}
When deserializing user we get username as expected, but null for Active. On the other hand when serializing data we get both isActive and username as expected. If we send request with active in query string it works as expected.
It's obviously problem with IModelBinder. It doesn't use DataMember's Name property for some reason. I checked what formaters are included and 4 default ones are registered:
System.Net.Http.Formatting.JsonMediaTypeFormatter
System.Net.Http.Formatting.XmlMediaTypeFormatter
System.Net.Http.Formatting.FormUrlEncodedMediaTypeFormatter
System.Net.Http.Formatting.JQueryMvcFormUrlEncodedFormatter
I don't have a way to check which one is used on request. I would assume that its FormUrlEncodedMediaTypeFormatter but I can't be sure. Also, I am not sure if it even supports Name property.
I already checked for a solution and closest topic I could find was WebAPI DataMember Name not used when de/serializing via application/x-www-form-urlencoded but it doesn't use [FromUri] but application/x-www-form-urlencoded property and it wasn't really solved.
Any ideas, pointers or suggestions would be much appreciated.
Use [FromQuery] instead other attributes.
And model for your request
http://localhost:8080/api/users?username=John&isActive=true
[Route("api/users")]
public class UsersController : Controller
{
[HttpGet]
public IHttpActionResult Get(User user)
{
//...
}
}
Will looks like
public class User
{
[FromQuery(Name = "username")]
public string Username{ get; set; }
[FromQuery(Name = "isActive")]
public bool? Active { get; set; }
}
Anyway best practice is to keep names in model as it parameters names in query.
In this case you dont have to provide "Name" parameter, only keep [FromQuery] on queryClass, and lower casing .Net provide automaticly.
You must check your "get" request. Your get request must be like this;
GET api/foo?username=fooname&active=false
You don't have to DataContract and DataMember attribute to just achieve this. These attribute just for another thing, its not the main reason to use for.
After get valid hit on your get method, in your method you can check modelstate like;
if (ModelState.IsValid) {
/// your code goes here
}
I'm facing little stranger issue with Web API controller. I have a collection which is being passed in an action of api controller. Object being used is collection is having 4 properties.
My action is able to accept collection parameter when it's properties are in specific order. See below :-
[HttpPost]
public ForexRates UpdateRates([FromBody] Rates rates)
{
// TODO: Obviously code :)
return rates;
}
This code is being place in API controller & calling from Postman. See below:-
<rates>
<rate>
<id>fefef</id>
<rate>35353.333</rate>
<series>dfefge</series>
<series-order>sfefefef</series-order>
</rate></rates>
If I change the order of the properties I started getting null value in my action. Can some one please explain this :)
Models
public class Rate
{
public string Id { get; set; }
public string Date { get; set; }
public double Rate { get; set; }
}
public class Rates : Collection<ForexRate>
{
}
You will need to control the order with which your XML is serialized. Use XmlElementAttribute and specify the Order.
There is a similar question here
FYI, I suppose there is no way for you to change the order of the properties, while you supply from PostMan to your WebApi service. You will need to follow the exact order.
If you don't wanna do that, then pass this Xml as a string parameter and then parse it inside a method.
The default binder can have issues when the same name is used in different places during binding.
In your case you've got Rate.Rate - both class name and property name. Try changing your class to (and corresponding xml for the post) :
public class Rate
{
public string Id { get; set; }
public string Date { get; set; }
public double Value { get; set; }
}
and then try changing the order.
While I don't have a definitive reason why it works in one order and not another, it's likely that when it gets to the Rate(double) value it tries to create a new Rate(object) but doesn't have the correct properties (as its just a double).
A more complicated solution would be to write a specific model binder for the Rate object.
The issue has to do with the DataContractSerializer which expects the elements to occur in a specific order (alphabetical with some consideration given to inheritance). That's the default serializer used when creating a Web API project.
You can override this and specify a different serializer during API Configuration like this:
GlobalConfiguration.Configuration.Formatters.XmlFormatter
.SetSerializer<SomeType>(new XmlSerializer(typeof(SomeType)));
I am posting a form in an asp.net-mvc page. Here is my controller action:
public ActionResult UpdateData(MyFormObject entity)
{
bool isValid = IsValid(entity);
if (!isValid)
{
var firstError = ModelState.Values.SelectMany(v => v.Errors).First();
throw new HttpException(404, firstError.ErrorMessage);
}
return Json(BuildResult(entity));
}
Even thought the post passes all of my explicit validation logic, when i check ModelState I see errors. I am seeing errors in ModelState when any of my properties are empty. Here is my object:
public class MyFormObject
{
public int Id{ get; set; }
public int TestId{ get; set; }
public int OtherId{ get; set; }
}
and I am looking at Model.State and i see errors for any element in my object that is not populated.
If I change this to (NOTE: the "?")
public class MyFormObject
{
public int? Id{ get; set; }
public int? TestId{ get; set; }
public int? OtherId{ get; set; }
}
then i no longer get any errors. Is there some default validation that is happening here that I am not setting. I am trying to figure out what is setting ModelState errors in the first case above.
When you are positing to a controller action that takes MyFormObject class object as a parameter, MVC engine will try to create an instance of that class via automatic model-binding. In order to create MyFormObject one needs to provide all of these:
public int Id{ get; set; }
public int TestId{ get; set; }
public int OtherId{ get; set; }
and if you don't provide at least any of these, it will try to assign null to the corresponding property. The int (value type) doesn't support null values, whereas int? does.
This is logically correct and actually helps you in a long run.
As #Maxim V. Pavlov said, when you post, ASP.MVC engine will try to validate the model, ie its class, and based on class you cited as example, the properties don't accepts a null or empty value, then it will throw an exception and ModelState will be invalid.
You can see more here # Validating Model Data in an MVC Application and here ModelStateDictionary.IsValid Property
You are probably submitting an empty MyFormObject to your UpdateData method. Value-types cannot be null and must be assigned a value. If a value is missing for a value-type, then it automatically will trigger a required field validation.
ASP.NET MVC even has a property that allows you to tweak this behaviour, although I believe it will only influence client-side validation:
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes