dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.25k stars 9.96k forks source link

ASP.NET CORE Web API: Value are always null when doing HTTP Post requests. #2202

Closed ghost closed 7 years ago

ghost commented 7 years ago

I'm experiencing a problem doing post requests via ASP.NET Core Web API. I'm a bit confused of the articles I read/found while Googling.

So here's my scenario:

I'm trying to do a HTTP Post request to an api endpoint with a complex parameter Example: [HttpPost] [Route("add")] public async Task PostStock([FromBody]Stock model) { if (model != null) { var isStockAdded = await _inventorySvc.AddStockAsync(model);

            if (isStockAdded)
                return Ok();
            else
                return BadRequest();
        }

        return BadRequest();
    }

}

My problem is that every time I make a post request to this endpoint the model value(s) are always null even if there's the [FromBody] parameter attribute or not.

Is this a bug in ASP.NET Core 2?

jkotalik commented 7 years ago

What does the Stock object look like? Also what are you sending to the endpoint?

ghost commented 7 years ago

Thanks for responding @jkotalik

Here's the structure of Stock class
public class Stock
    {
        [Key]
        public Guid StockId { get; set; }

        [Required]
        [DataType(DataType.Text)]
        public string StockName { get; set; }

        [Required]
        [DataType(DataType.Text)]
        public Guid SKU { get; set; }

        public string Description { get; set; }

        [Required]
        public int Qty { get; set; }

        [Required]
        public DateTime DateAdded { get; set; }

        [Required]
        public decimal UnitPrice { get; set; }

        [Required]
        public decimal MarkupPrice { get; set; }

        [Required]
        public decimal SellingPrice { get; set; }

        [ForeignKey("UserId")]
        public virtual ApplicationUser User { get; set; }

        [Required]
        public Guid UserId { get; set; }
    }

This is the data I'm sending to the endpoint using Postman:

{
    "StockId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "StockName":"test item",
    "SKU":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "Description":"test item",
    "Qty":10,
    "DateAdded":"XXXXXXXXXXX",
    "UnitPrice":00,
    "MarkupPrice":00,
    "SellingPrice":00,
    "UserId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
jkotalik commented 7 years ago

Most likely one of these properties failed to bind. @rynowak I don't know who is in charge of model binding, but you have any ideas?

zzyykk123456 commented 7 years ago

@lorenz31 I have tried like this and it does work. qq 20170919104338

zzyykk123456 commented 7 years ago

@lorenz31 Stock like this: { "stockId": "8fc759e0-0da1-478d-96b7-d939e4498d48", "stockName": "string", "sku": "8fc759e0-0da1-478d-96b7-d939e4498d48", "description": "string", "qty": 0, "dateAdded": "2017-09-19T02:37:28.937Z", "unitPrice": 0, "markupPrice": 0, "sellingPrice": 0, "userId": "8fc759e0-0da1-478d-96b7-d939e4498d48" }

jkotalik commented 7 years ago

Thanks for checking that @zzyykk123456.

ghost commented 7 years ago

@zzyykk123456 But how come the prices doesn't have the actual values being passed upon request?

zzyykk123456 commented 7 years ago

@lorenz31 It works too. 1 2

ghost commented 7 years ago

@zzyykk123456 I'm using Postman to test it but it doesn't pass the values even with or without the [FromBody] parameter attribute.

zzyykk123456 commented 7 years ago

@lorenz31 With or without [FromBody] is ok.The postman setup is here.

1 2

ghost commented 7 years ago

@zzyykk123456 Hmm in VS 2017 I really can't get the values being passed to the endpoint upon the request. But as what you showed in your samples, this makes me confused.

ghost commented 7 years ago

@zzyykk123456 I created the asp.net core web api project via VS 2017 Community edition version 15.3.4.

ghost commented 7 years ago

@zzyykk123456 This is just a class derived from DbContext.

ghost commented 7 years ago

@zzyykk123456 Sorry, I was mistaken. Its just a class with 3 properties

public class ApplicationUser { [Key] public Guid Id { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    public virtual ICollection<Stock> Stocks { get; set; }
}

NOTE: I'm not using Asp.Net Identity here.

ghost commented 7 years ago

@zzyykk123456 Hmm so what do you think is the problem? How come that it can't receive the values being passed from postman to the api controller when making the POST request?

ghost commented 7 years ago

@zzyykk123456 I have the same setup as what you have.

ghost commented 7 years ago

@zzyykk123456 Here's my setup

On Postman screenshot

On Visual Studio 2017 Community Edition screenshot

kirst commented 7 years ago

@lorenz31 could you try camelCase for your property names in postman

ghost commented 7 years ago

@kirst It still doesn't work.

zzyykk123456 commented 7 years ago

@lorenz31 Your StockId and SKU guid are invalid. You can get new guid in https://www.guidgenerator.com/online-guid-generator.aspx. Try it.

ghost commented 7 years ago

@zzyykk123456 That was just a sample. I'm actually generating new stockid and sku via the _inventorySvc.AddStockAsync() method. I just put it there to fill the field.

jayslife commented 7 years ago

Can you tell us what ModelState.IsValid returns? If it returns false I would evaluate ModelState to see why.

jayslife commented 7 years ago

I also have a feeling that the signature with string sellerid has something to do with your issue. Where is that being populated from? If I have a signature like that I typically pass that via query string param.

You can change your method to look something like this:


[HttpPost]
[Route("add/{sellerId}")]
public async Task PostStock([FromBody]Stock model, string sellerId)
{
if (model != null)
{
var isStockAdded = await _inventorySvc.AddStockAsync(model);

            if (isStockAdded)
                return Ok();
            else
                return BadRequest();
        }

        return BadRequest();
    }
}

Then post to: http://localhost:49828/api/TokenAuth/add?sellerId=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
ghost commented 7 years ago

@jayslife The prefix route is "api/Inventory/{sellerid}" and the route for PostStock() is "add" so there will be "api/Inventory/{sellerid}/add".

duanemck commented 7 years ago

@lorenz31 Did you solve your issue? I seem to be experiencing the same thing.

ghost commented 7 years ago

@duanemck Fortunately yes, I already solved my issue :). It turns out that I shouldn't be passing the StockId and SKU fields along with the request because those fields are filled in my service class (generating new Guid). That's my case, it works now.

Could you post a code snippet of your web api and sample request? Hopefully I might help you.

duanemck commented 7 years ago

Luckily I just found my issue too. I had some custom middleware for request logging that was reading the stream and not resetting it, so the modelbinder had nothing to bind to. Spent all day chasing such a silly thing. Glad yours was sorted too, thanks for the offer to help.

akakira commented 6 years ago

Not much guesswork in these issues really, look at the ModelState object and see what binding errors occurred. That usually tells you 99% of the time.

ebeyer1 commented 6 years ago

Not much guesswork in these issues really, look at the ModelState object and see what binding errors occurred. That usually tells you 99% of the time.

@akakira Can't believe I didn't think to check ModelState. I wasted hours trying to figure out a similar issue, and ModelState helped point it out. Thanks for the tip!

akakira commented 6 years ago

No problem, cheers to you mate.

trktuhin commented 6 years ago

Hello @lorenz31 I am facing similar problem . can u help me with this? Here is my api for uploading an image:

public async Task Upload(int vId,IFormFile file) { var vehicle = await this.repository.GetVehicle(vId, hasAdditional: false); if (vehicle == null) return NotFound(); var uploadsFolderPath = Path.Combine(host.WebRootPath, "uploads"); if (!Directory.Exists(uploadsFolderPath)) Directory.CreateDirectory(uploadsFolderPath); var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName); var filePath = Path.Combine(uploadsFolderPath, fileName);

error shows on this line:

    var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);

that indicates its not getting the file while I am sending a image file with the same key "file". Everything else works fine.

ghost commented 6 years ago

@trktuhin I think you shouldn't use Path.GetExtension() since that returns only the file extension not the full name of the file with its extension.

Aryan2808 commented 6 years ago

Hey, I have the same issue but I figure out that my model contains a DateTime type value and it is not getting set.Can anybody tell me how to solve this issue ?

zorkind commented 6 years ago

I have a int field being completely ignored by modelbinder. It just don't show up on ModelState at all, and it is in the request i can even access it with Request.Form["field"] it's there, modelbinder just chose to ignore it. When i try to bind it manually i get something weird like "The runtime refused to evaluate the expression at this time." but if i set the value itself, it works, only not if i am setting it from the Form. It has a weird Microsoft.Extensions.Primitives.StringValues type on it, i did a GetType and it gave me this dude. But it's just a simple NUMBER. It should be int. :-(

zorkind commented 6 years ago

Ok so i figured out my issue.

For some reason the ModelBinder don't like to have an Object property in the model.

public object Id { get; set; }

This gets absolutely and utterly ignored by model binder :-( weird AF.

gjreige commented 6 years ago

I also figured out what my issue was. I tried all the solutions above and realised after a lot of testing what the issue was.

As silly as it sounds, in one of my string fields in the model I was passing in a local file path:

{ "path": "c:\" }

The '\' was escaping the second quotation mark causing invalid json to be passed through.

You can include '\' in your fields by using a double '\\'

{ "path": "c:\\" }

dancodehq commented 5 years ago

I'm getting the same thing. My controller doesn't bind the model unless it has [FromBody] infront of it. In the variant that doesn't work I just get nulls and default values.

Doesn't work:

[HttpPost]
public IActionResult Post(Product product)
}
    // ... code ...
}

Works:

[HttpPost]
public IActionResult Post([FromBody]Product product)
}
    // ... code ...
}

Edit: this article seems to suggest that [FromBody] is required whenever we are posting a JSON model to our methods?

kuromukira commented 5 years ago

Can't seem to fix the same issue on my end. My model only receives null / default values whenever I do HttpPost. Both in postman and Angular7 (typescript). Tried adding [FromBody] and Content-Type: application/json when doing requests, still no luck.

kuromukira commented 5 years ago

Can't seem to fix the same issue on my end. My model only receives null / default values whenever I do HttpPost. Both in postman and Angular7 (typescript). Tried adding [FromBody] and Content-Type: application/json when doing requests, still no luck.

Resolved mine. Removing [JsonArray] was my solution. But still with [FromBody] and Content-Type: application/json as header.

wahyuam commented 5 years ago

I'm getting the same thing. My controller doesn't bind the model unless it has [FromBody] infront of it. In the variant that doesn't work I just get nulls and default values.

Doesn't work:

[HttpPost]
public IActionResult Post(Product product)
}
    // ... code ...
}

Works:

[HttpPost]
public IActionResult Post([FromBody]Product product)
}
    // ... code ...
}

Edit: this article seems to suggest that [FromBody] is required whenever we are posting a JSON model to our methods?

Thanks, Its Work!

marouaneterai commented 5 years ago

When sending data to an endpoint, there is a difference between setting the Id to null and not setting an Id attribut at all. In my case, when i send data like this, an Image object will be created with those data, and will be saved in the database correctly with a new Id:

{
    "Name": "00001.jpg",
    "Title": "Image 1 title",
    "Description": "Image 1 description",
    "Created": "03/04/2019",
    "Modified": "03/04/2019"
}

but when i send data like this:

{
    "Id": null
    "Name": "00001.jpg",
    "Title": "Image 1 title",
    "Description": "Image 1 description",
    "Created": "03/04/2019",
    "Modified": "03/04/2019"
}

This will instantiate a new object and save it in database but without the data i sended to the api.

ancor1369 commented 5 years ago

Hello, I used to have the same problem in my code, however I realized that the problem was very simple to solve, yes after I almost drive myself crazy!

It is necessary to make sure that the controller is decorated with [ApiController], other wise, the object you send from postman over http, will not arrive to the method.


    [Route("api/[controller]")]
    [ApiController] //<- Make sure it is there
    public class ApplicantDocumentController : Controller
    {
       .
       .
       .
        [HttpPost]
        public async Task<ActionResult<ApplicantDocument>> post(ApplicantDocument AppDoc)
        {
           .
           .
           .
        }
}

This is a request without the decorator

image

This is a request with the decorator

image

dcarr42 commented 5 years ago

@ancor1369 Possibly due to the ApiController applying binding source parameter inference....

jvargasvillegas commented 5 years ago

The same bug that @ancor1369 mentioned.

Once I decorated with the attribute "api/[controller]" it performs.

image

I almost get crazy!

JohnnyFun commented 5 years ago

For those who ran into what @duanemck pointed out, where some middleware is reading the body and then later on in the pipeline your model binder gets null, this middleware at the top of my pipeline resolved the issue for me:

public class EnableRewindableBodyStartup {
    private readonly RequestDelegate _next;

    public EnableRewindableBodyStartup(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext httpContext) => await RewindBodyStreamAndNext(httpContext, _next);

    private async Task RewindBodyStreamAndNext(HttpContext context, RequestDelegate next) {
        context.Request.EnableRewind(); // extension method in `Microsoft.AspNetCore.Http.Internal`
        await next(context);
    }
}

in startup call it like:

app.UseMiddleware<EnableRewindableBodyStartup>();
...add other middleware (audit log, global error, static files, mvc, etc)
IronRate commented 5 years ago

services .AddMvc() .AddJsonOptions(options => { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            });

This resolve for me