Customizing Web API Controller Parameters - rest

We have a Web API Controller generated from a model. The model has this form:
public class Pdf
{
public int ID { get; set; }
public string Name { get; set; }
public string Url { get; set; } // File stored in AWS
public int Job_ID { get; set; }
public List<PdfPage> { get; set; }
}
The automatically generated controller has this default POST route:
// POST: api/Pdfs
[ResponseType(typeof(Pdf))]
public async Task<IHttpActionResult> PostPdf(Pdf pdf)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.JobFiles.Add(pdf);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = pdf.ID }, pdf);
}
In making a new Pdf, however, the file itself is put in AWS. In the case of hitting this route, where we're making a new Pdf, we want to make a new PDF that will be uploaded to AWS. In order to do that, we want to pass additional parameters to the route that don't exist in the model. Something like this:
// POST: api/Pdfs
[ResponseType(typeof(Pdf))]
public async Task<IHttpActionResult> PostPdf(Pdf pdf, double heightInches, double widthInches)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Make a blank PDF based on the dimensions provided
MemoryStream newBlankPdfMemoryStream = PdfOperations.ITextSharpNewBlankPdf(8.5, 11);
pdf.Url = await AWS.S3PubliclyAccessibleInsert("paragonpdfimages", pdf.Job_ID + "/" + pdf.Name, newBlankPdfMemoryStream);
db.JobFiles.Add(pdf);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = pdf.ID }, pdf);
}
However, passing this body to the POST route:
{pdf: {Job_Id: 395, Name: "fds"}, widthInches: 8.5, heightInches: 11}
Results in a "405 Method Not Allowed" and a message in the body of "The requested resource does not support http method 'POST'".
How can I accomplish this passing of additional parameters to the POST route? Do I need to make a custom route? If so what would that look like? Thanks in advance.

The problem you're having is caused by the fact that by default Web-Api binds parameters with a simple data type (such as double) using values from the query string of the URL. So you have 2 options:
Tell Web-Api to get the values from the body by modifying your action to be
public async Task<IHttpActionResult> PostPdf(Pdf pdf, [FromBody]double heightInches, [FromBody]double widthInches)
Pass the height and width values in the query string and not in the body.
....../PostPdf?heightInches=11&widthInches=8.5

Related

How to get a upload button in swagger for IFormFile combined with other properties?

I have created a Asp.net core 3.1 web api with Swagger to upload files to server. the following code is working fine:
[HttpPost("PostFile")]
public ActionResult PostFile(IFormFile uploadedFile)
{
var saveFilePath = Path.Combine("c:\\savefilepath\\", uploadedFile.FileName);
using (var stream = new FileStream(saveFilePath, FileMode.Create))
{
uploadedFile.CopyToAsync(stream);
}
return Ok();
}
I get a nice upload button in swagger when I try to run this.
However, now I wanted to use a different model. that has some more properties along with the IFormFile.
public class FileUploadRequest
{
public string UploaderName { get; set; }
public string UploaderAddress { get; set; }
public IFormFile File { get; set; }
}
When I try to use this model, I dont see any upload button in Swagger that will help me to attach the file in the request.
For some reason, it shows the IFormFile as String. How can I get a upload button here?
In ASP.NET Core Web API, it binds application/json format data by default. But what your model need is multipart/form-data type data. So you need [FromForm] attribute to specific the source.
I use Swashbuckle.AspNetCore version 5.6.3 in ASP.NET Core 3.1:
[HttpPost]
public ActionResult PostFile([FromForm]FileUploadRequest model)
{
}
Result:
You can use IFormFile again in [HttpPost] so you can see the button.
public class FileUploadRequest
{
public string? UploaderName { get; set; }
public string? UploaderAddress { get; set; }
public IFormFile? File { get; set; }
}
[HttpPost]
public IActionResult PostFile(IFormFile file, FileUploadRequest model)
{
var saveFilePath = Path.Combine("c:\\savefilepath\\", model.UploaderAddress!);
using (var stream = new FileStream(saveFilePath, FileMode.Create))
{
file.CopyToAsync(stream);
}
return Ok();
}

Model Attribute binding in PUT Web API not wokring - ASP.NET Core 3.1

I have a PUT Rest API that I want to do binding from both body and route parameters.
Code
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus([FromBody] StatusRequest StatusRequest)
{
// code ...
}
And the model class is
public class StatusRequest
{
[FromRoute(Name = "Id")]
[Required(ErrorMessage = "'Id' attribute is required.")]
public string Id { get; set; }
[FromBody]
[Required(ErrorMessage = "'Status' attribute is required.")]
public string Status { get; set; }
}
When I made a request to this API, the Id is not mapped to the model even though I added the FromRoute attribute explicitly. Any suggestions?
The [FromBody] model binding will effectively override the [FromRoute] option in your model class. This is by design (why, I'm not sure, but an MS decision). See the "[FromBody] attribute" section of this doc: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding. As pointed out there: "When [FromBody] is applied to a complex type parameter, any binding source attributes applied to its properties are ignored." So adding the "[FromRoute]" attribute inside your model does nothing...it's ignored. You can remove both of those attributes from your model.
So the way around this is to put the route binding in the Put action as a method parameter and then manually add it to your model in the controller before using the model.
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus(int Id, [FromBody] StatusRequest StatusRequest)
{
StatusRequest.Id = Id;
// remaining code...
}
The downside to this method is that the Required attribute cannot remain on the Id parameter. It will be null at the time of model binding and if you have .Net Core 3.1 automatic model validation active, then that will always return a 422. So if you would have to manually check that yourself before adding it to the model.
If you want even more flexibility, you can look at something like the HybridModelBinding NuGet package that allows various combinations of model binding using attributes. But this is a 3rd party dependency that you may not want. (https://github.com/billbogaiv/hybrid-model-binding/)
You can use custom model binding,here is a demo:
TestModelBinderProvider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
this.formatters = formatters;
this.readerFactory = readerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(StatusRequest))
return new StatusBinder(formatters, readerFactory);
return null;
}
}
Startup.cs:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
StatusBinder:
public class StatusBinder: IModelBinder
{
private BodyModelBinder defaultBinder;
public StatusBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
defaultBinder = new BodyModelBinder(formatters, readerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// callinng the default body binder
await defaultBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
var data = bindingContext.Result.Model as StatusRequest;
if (data != null)
{
var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
data.Id = value.ToString();
bindingContext.Result = ModelBindingResult.Success(data);
}
}
}
}
StatusRequest:
public class StatusRequest
{
[Required(ErrorMessage = "'Id' attribute is required.")]
public string Id { get; set; }
[Required(ErrorMessage = "'Status' attribute is required.")]
public string Status { get; set; }
}
Action:
[HttpPut("{Id}/someStuffApi")]
public ActionResult UpdateStatus(StatusRequest StatusRequest)
{
return Ok();
}
result:

POST parameter is null in when it's a hand made type

I have a asp.net 4.5.2 web api and one POST method receive a DTO parameter I've coded by hand like these:
using System;
namespace WebApi.Models.DTO
{
[Serializable]
public class MyModelDto
{
public MyModelDto()
{
}
public int Id { get; set; }
public string Code { get; set; }
public int Type { get; set; }
}
}
When I call this method from Postman the parameter is always null, but if I design the same DTO class in EntityFramewok (edmx model) and use it as parameter I could receive the data inside the post method as it was send.
I could not realize what I'm missing in my hand made class?
Why it works using EF and not with my class?
Setting the parameter as dynamic also works...
[HttpPost]
[JwtAuthentication]
[Route("api/bills/invoice")]
public JsonResult<WebApi.Models.DTO.MyModelDto> Post(WebApi.Models.DTO.MyModelDto param)
{
// Here param is always NULL
}
[HttpPost]
[JwtAuthentication]
[Route("api/bills/invoice")]
public JsonResult<Data.MyModelDto> Post(Data.MyModelDto param)
{
// Here param wors! based on the EF class
}
public JsonResult<Data.MyModelDto> Post(dynamic param)
{
// Here param wors! using dynamic data type
}
it depends how you send the data via Postman.
I don't have Postman here to test but make sure you select Json Type when sending data and then your sent data needs to match your DTO. Something like:
{
"Id":1,
"Type":"whatever",
"Code":"whatever"
}
This data goes in the body of the request since you are sending a Post request.
One extra observation, your DTO does not need an empty constructor, you can get rid of it. There is nothing wrong with:
public class MyModelDto
{
public int Id { get; set; }
public string Code { get; set; }
public int Type { get; set; }
}
For testing purposes get rid of your Jwt authentication, until you get the calls running correctly

How do I fix a 400 Bad Request error in .Net Core POST operation?

I have an .Net Core 2.1 API that posts data using EF core. When I make a POST request from Postman to http://localhost:3642/task/create I get a 400 Bad Request Error (The request cannot be fulfilled due to Bad Syntax).After digging around I got a suggestion to comment out the ValidateAntiForgery token from the controller. When I pass the request from postman with this change I get 200 Ok status message but no data is being committed to the table in Sql Server. Is there something that I should configure in my API, something else am I missing?
My controller looks as follows:
[HttpPost]
// [ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("Assignee,Summary,Description")] TaskViewModel taskViewModel)
{
if (ModelState.IsValid)
{
_context.Add(taskViewModel);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View();
}
In TaskViewModel.cs I have:
public class TaskViewModel
{
[Required]
public long Id { get; set; }
[Required(ErrorMessage = "Please provide Task Summary")]
[Display(Name = "Summary")]
public string Summary { get; set; }
[Required(ErrorMessage = "Please enter task description")]
[Display(Name = "Description")]
public string Description { get; set; }
[Required(ErrorMessage = "Please select Assignee")]
[Display(Name = "Assign To")]
public string Assignee { get; set; }
}
This is my payload in Postman:
{
"Assignee": "Ed tshuma",
"Summary": "Finish reconciliations",
"Description": "collate all the pending data"
}
There's a number of issues here. First and foremost, why are you saving your view model to the database. This is actually an entity in this case, not a view model. You should definitely be using a view model, but you should also have a separate entity class. Then, your view model should only contain properties that you want to actually allow the user to edit, negating the need entirely for the Bind attribute, which should be avoided anyways. (see: Bind is Evil).
// added "Entity" to the name to prevent conflicts with `System.Threading.Task`
[Table("Tasks")]
public class TaskEntity
{
[Key]
public long Id { get; set; }
[Required]
public string Summary { get; set; }
[Required]
public string Description { get; set; }
[Required]
public string Assignee { get; set; }
}
public class TaskViewModel
{
[Required(ErrorMessage = "Please provide Task Summary")]
[Display(Name = "Summary")]
public string Summary { get; set; }
[Required(ErrorMessage = "Please enter task description")]
[Display(Name = "Description")]
public string Description { get; set; }
[Required(ErrorMessage = "Please select Assignee")]
[Display(Name = "Assign To")]
public string Assignee { get; set; }
}
Also, note the division of responsibility. The entity has only things that matter to the database ([Required] here indicates that the column should be non-nullable). Whereas the view model is concerned only with the view. There's no Id property, since it's not needed or desired, and the display names and error messages to be presented to the user are placed here only.
Then, you'll need to map from your view model to your entity class:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(TaskViewModel model)
{
if (!ModelState.IsValid)
return View(model);
var task = new TaskEntity
{
Assignee = model.Assignee,
Summary = model.Summary,
Description = model.Description
};
_context.Add(task);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
The mapping here is fairly straight-forward, but you may prefer to utilize a library like AutoMapper to handle this for you: _mapper.Map<TaskEntity>(model).
While this is specifically for a create action, it's worth pointing out the subtle difference for an update. You'll want to first retrieve the existing task from your database and then map the posted values onto that. The rest remains relatively the same:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(long id, TaskViewModel model)
{
if (!ModelState.IsValid)
return View(model);
var task = await _context.Tasks.FindAsync(id);
if (task == null)
return NotFound();
task.Assignee = model.Assignee;
task.Summary = model.Summary;
task.Description = model.Description;
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Finally, as to the main problem from your question, there's two issues. First, this action is designed for a traditional HTML form post (x-www-form-urlencoded). As such, it doesn't make sense to send JSON to it, and sending JSON to it will not work. To test it in Postman, you should send the request as x-www-form-urlencoded. If you do not, then your model will essentially always be invalid, because nothing will be bound to your model from the post body.
In order to receive JSON, your param would need to have the FromBody attribute applied to it ([FromBody]TaskViewModel model). However, if you do that, you can no longer receive traditional form posts, and in this context, that's what's going to be sent. If you were sending via AJAX (where you could conceivably use JSON), then you should also be returning JSON or maybe PartialView, but not View or a redirect.
Lastly, you need to include the request verification token, which should be another key in the post body name __RequestVerificationToken. To get the value to send, you'll need to load the GET version of the view, first, and inspect the source. There will be a hidden input with the value.
Chris Pratt is right, you need to send __RequestVerificationToken.
If you comment out [ValidateAntiForgeryToken] attribute, it seems that you send data from Body-raw-JSON, then you need to use [FromBody] to access data.
[HttpPost]
public async Task<IActionResult> Create([Bind("Assignee,Summary,Description")] [FromBody] TaskViewModel taskViewModel)
If you do not want to add [FromBody], you could send data using form-data
You have to send the anti forgery token with your request if you want to use the decorator [ValidateAntiForgeryToken]. See this link for more information.
Also, even if your model is invalid, you return View(). That means you get a http status 200 even if you send wrong data.
Set a breakpoint on if(ModelState.IsValid) and check if you enter in it. If not, check the format of your payload.
Hope it helps.
EDIT regarding your payload and your model : You need to provide an Id to you payload because of the [Required] decorator in your TaskViewModel. Or you need to get rid of the [Required] attribute on Id. If you don't, if (ModelState.IsValid) will always be false.

How to pass query string parameter to asp.net web api 2

How can I pass more than 1 parameters as part of query string to my asp.net web api 2.
This is my asp.net web api 2 method, I am not able to figure out that how can I decorate this method so that it accepts the id and a complex type which is CustomerRequest, I want to use Url something like
http://localhost/api/Customer/?Mobile0012565987&Email=abcxyz.com&IsEmailVerified=true
[ResponseType(typeof(Customer))]
public IHttpActionResult GetCustomer(long id, [FromUri]CustomerRequest request)
{
var customer = db.Customers.Find(request.CustomerId);
if (customer == null)
{
return NotFound();
}
return Ok(customer);
}
This is CustomerRequest class
public class CustomerRequest
{
public string Mobile { get; set; }
public string Email { get; set; }
public Nullable<bool> IsEmailVerified { get; set; }
}
Otherwise pleaase guide me if there is a better way to do it.
Thanks
Based on your code, you need to pass 'id' as well, like this:
http://localhost/api/Customer/?id=12345&Mobile=0012565987&Email=abcxyz.com&IsEmailVerified=true
if you want to make 'id' optional, you can make your method signature look like this:
public IHttpActionResult GetCustomer([FromUri]CustomerRequest request, long id = 0)
this will set id to 0 by default, if you dont pass it in the URL. So you will be able to access your URL like you originally did:
http://localhost/api/Customer/?Mobile=0012565987&Email=abcxyz.com&IsEmailVerified=true