I'm creating a RESTful service using ServiceStack that should consume a POST with multipart/form-data content. The content is in JSON format, but when I send the POST, the object is not deserialized correctly (all properties are null/default values). If I try just sending that object as a regular POST (without the multipart/form-data), it deserializes just fine.
I poked around in the ServiceStack code to try to figure out what was going on, and this is my current understanding:
HttpListenerRequestWrapper::LoadMultiPart() is loading the multipart request and saving the (non-file) parts to "FormData", which maps the name of the part to its contents. However, it appears the content-type (which is correctly written to the HttpMultiPart::Element as the individual sections are being parsed) is lost because it isn't stored in anywhere.
Some time later in control-flow, EndpointHandlerBase::DeserializeHttpRequest() calls KeyValueDataContractDeserializer.Instance.Parse() with the FormData and the type to deserialize to.
If this is the first time that kind of object is being deserialized, a StringMapTypeDeserializer is created for that type and cached to typeStringMapSerializerMap. For each property of the type, we call JsvReader.GetParseFn() to get a ParseStringDelegate to parse that deserialize that property.
The created/cached StringMapTypeDeserializer is then used to deserialize the object, using all the "ParseFn's" set earlier... which all treat the content as JSV format.
I confirmed that JsvReader.ParseFnCache has a bunch of types in it, while JsonReader.ParseFnCache is empty. Also, if I change my request to remove all quotes (i.e. turn it from JSON into JSV format), it deserializes correctly. The one weird thing is that one of the properties of my object is a Dictionary, and that deserializes correctly, even when it's in JSON format; I'm assuming this is just a fortunate coincidence (?!?).
Am I correct in my understanding of what's going on here? Is this a known limitation in ServiceStack? Bug? Is there anyway to work around it other than putting my object in a file and manually calling JsonSerializer.DeserializeFromStream()?
Thanks!
jps
Also, just incase it's useful, here's the relevant request and data-objects:
POST /api/Task HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary=Boundary_1_1161035867_1375890821794
MIME-Version: 1.0
Host: localhost:12345
Content-Length: 385
--Boundary_1_1161035867_1375890821794
Content-Type: application/json
Content-Disposition: form-data; name="MyMap"
{"myfile.dat":"ImportantFile"}
--Boundary_1_1161035867_1375890821794
Content-Disposition: form-data; name="MyThing"
Content-Type: application/json
{"Id":123,"Name":"myteststring"}
--Boundary_1_1161035867_1375890821794
Content-Type: application/octet-stream
Content-Disposition: form-data; filename="myfile.dat"
mydatagoeshere...
--Boundary_1_1161035867_1375890821794--
.
public class TestObj
{
public long Id { get; set; }
public string Name { get; set; }
}
[Route("/Task", "POST")]
public class TaskRequest : AuthenticatedRequest, IReturn<TaskResponse>
{
public TestObj MyThing { get; set; }
public Dictionary<string, string> MyMap { get; set; }
}
Have you tried setting the properties of your request object using the 'ApiMember' attribute? In particular the 'ParameterType' properties.
/// <summary>
/// Create and upload a new video.
/// </summary>
[Api("Create and upload a new video.")]
[Route("/videos", "POST", Summary = #"Create and upload a new video.",
Notes = "Video file / attachment must be uploaded using POST and 'multipart/form-data' encoding.")]
public class CreateVideo : OperationBase<IVideo>
{
/// <summary>
/// Gets or sets the video title.
/// </summary>
[ApiMember(Name = "Title",
AllowMultiple = false,
DataType = "string",
Description = "Video title. Required, between 8 and 128 characters.",
IsRequired = true,
ParameterType = "form")]
public string Title { get; set; }
/// <summary>
/// Gets or sets the video description.
/// </summary>
[ApiMember(Name = "Description",
AllowMultiple = false,
DataType = "string",
Description = "Video description.",
ParameterType = "form")]
public string Description { get; set; }
/// <summary>
/// Gets or sets the publish date.
/// </summary>
/// <remarks>
/// If blank, the video will be published immediately.
/// </remarks>
[ApiMember(Name = "PublishDate",
AllowMultiple = false,
DataType = "date",
Description = "Publish date. If blank, the video will be published immediately.",
ParameterType = "form")]
public DateTime? PublishDate { get; set; }
}
Related
I instantiated an object from the following class and shoved that into a Blazored server localstorage element.
public class StatusData{public string Message { get; set; } = string.Empty; }
When I changed the string property to a field, my Blazor app ran, but the app could not retrieve the localstorage object. I do not know if the object was put into localstorage, or if the retriever did not work.
public class StatusData { public string Message = string.Empty; }
I added the { get; set; } back in and the localstorage worked again.
I expected that using Message as a field or a property would have worked the same.
Can someone help me understand the difference?
This library uses System.Text.Json as its default Json library, and that has a default of ignoring fields.
You can configure it to include fields though.
In your startup/program file you need to configure the JsonSerializerOptions
builder.Services.AddBlazoredLocalStorage(config =>
config.JsonSerializerOptions.IncludeFields = true
);
README
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.
I am trying to create an email service web api with multiple attachments. Need to have only one parameter for web api. But, right now I have 2 complex parameters, which web api doesn't accept. Please suggest how do I implement multiple attachments and use only one complex parameter for web api.
[HttpPost]
[ActionName("sendemail")]
public IHttpActionResult processEmail(EmailModel emailModel,
List<HttpPostedFileBase> attachments)
{
........
}
EmailModel
public class EmailModel
{
public string ToAddress { get; set; }
public string FromAddress { get; set; }
public string Body { get; set; }
}
And, in Email Controller I use the list to attach the attachments to MailMessage object.
foreach (HttpPostedFileBase attachment in attachments)
{
if (attachment != null)
{
string fileName = Path.GetFileName(attachment.FileName);
//create linked resource for embedding image
LinkedResource pic = new LinkedResource(attachment.InputStream, "image/jpg");
//add linked resource to appropriate view
htmlView.LinkedResources.Add(pic);
}
}
//add view
msg.AlternateViews.Add(htmlView);
One solution would be to accept your email model as your parameter, and grab the uploaded files in the HttpContext.Request.Files to loop over. That's assuming you are posting with multipart/form-data
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
What is wrong with ServiceStack.Text.XmlSerializer ?
I have object:
public class weatherdata : IReturn<WashService>
{
public Location location { get; set; }
}
public class Location
{
public string name { get; set; }
public string country { get; set; }
}
Try to deserialize thirdparty xml like that:
var data = ServiceStack.Text.XmlSerializer.DeserializeFromString<weatherdata>("<weatherdata><location><name>Moscow</name><country>RU</country></location></weatherdata>");
data.location.name = Moscow.
data.location.country is NULL;
Change xml like that:
var data = ServiceStack.Text.XmlSerializer.DeserializeFromString<weatherdata>("<weatherdata><location><country>RU</country><name>Moscow</name></location></weatherdata>");
and see
data.location.name == "Moscow".
data.location.country =="RU";
Why so different results if I only change order?
As explained here, the default XML serializer used by ServiceStack (.NET's DataContract serializer) assumes that the XML elements must be in the same order as declared in your class. In XML schema terminology, the elements are declared as xs:sequence rather than xs:all. If you need to support XML elements in any possible ordering in the request, then you may need to override the XML serializer used by ServiceStack as explained in the link above.
If you just need to adjust the ordering of the XML elements, I believe you can specify an exact ordering for your elements by decorating your properties with DataMember attributes and specifying the Order property. If you do this, then you will also need to decorate your Location class with a DataContract attribute.