ServiceStack not binding FormData on multi-part request - file-upload

I'm performing file uploads from Javascript. The file is transferred fine, but the additional form data passed in the request is not bound to the request DTO.
From Chrome inspector:
------WebKitFormBoundaryunl7tsdqzGBvtsUH
Content-Disposition: form-data; name="albumId"
1037
------WebKitFormBoundaryunl7tsdqzGBvtsUH
Content-Disposition: form-data; name="file"; filename="Tulips.jpg"
Content-Type: image/jpeg
RequestDTO
public class UploadRequest : IRequiresRequestStream
{
public Stream RequestStream { get; set; }
public string FileName { get; set; }
public long? AlbumId { get; set; }
}
The image is properly bound, but other items from form-data. What's interesting is that
Request.FormData contains the entry for albumId.
Any clues ?

I think this is due to the fact that UploadRequest is inheriting from
IRequiresRequestStream thus bypassing any binding of form data to the DTO.

Related

Model Binding for multipart/form-data (File + JSON) post in ASP.NET Core 1.1

I'm attempting to build an ASP.NET Core 1.1 Controller method to handle an HTTP Request that looks like the following:
POST https://localhost/api/data/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------625450203542273177701444
Host: localhost
Content-Length: 474
----------------------------625450203542273177701444
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: text/plain
<< Contents of my file >>
----------------------------625450203542273177701444
Content-Disposition: form-data; name="text"
Content-Type: application/json
{"md5":"595f44fec1e92a71d3e9e77456ba80d0","sessionIds":["123","abc"]}
----------------------------625450203542273177701444--
It's a multipart/form-data request with one part being a (small) file and the other part a json blob that is based on a provided specification.
Ideally, I'd love my controller method to look like:
[HttpPost]
public async Task Post(UploadPayload payload)
{
// TODO
}
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
But alas, that doesn't Just Work {TM}. When I have it like this, the IFormFile does get populated, but the json string doesn't get deserialized to the other properties.
I've also tried adding a Text property to UploadPayload that has all the properties other than the IFormFile and that also doesn't receive the data. E.g.
public class UploadPayload
{
public IFormFile File { get; set; }
public UploadPayloadMetadata Text { get; set; }
}
public class UploadPayloadMetadata
{
[Required]
[StringLength(32)]
public string Md5 { get; set; }
public List<string> SessionIds { get; set; }
}
A workaround that I have is to avoid model binding and use MultipartReader along the lines of:
[HttpPost]
public async Task Post()
{
...
var reader = new MultipartReader(Request.GetMultipartBoundary(), HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
var filePart = section.AsFileSection();
// Do stuff & things with the file
section = await reader.ReadNextSectionAsync();
var jsonPart = section.AsFormDataSection();
var jsonString = await jsonPart.GetValueAsync();
// Use $JsonLibrary to manually deserailize into the model
// Do stuff & things with the metadata
...
}
Doing the above bypasses model validation features, etc. Also, I thought maybe I could take that jsonString and then somehow get it into a state that I could then call await TryUpdateModelAsync(payloadModel, ...) but couldn't figure out how to get there either - and that didn't seem all that clean either.
Is it possible to get to my desired state of "transparent" model binding like my first attempt? If so, how would one get to that?
The first problem here is that the data needs to be sent from the client in a slightly different format. Each property in your UploadPayload class needs to be sent in its own form part:
const formData = new FormData();
formData.append(`file`, file);
formData.append('md5', JSON.stringify(md5));
formData.append('sessionIds', JSON.stringify(sessionIds));
Once you do this, you can add the [FromForm] attribute to the MD5 property to bind it, since it is a simple string value. This will not work for the SessionIds property though since it is a complex object.
Binding complex JSON from the form data can be accomplished using a custom model binder:
public class FormDataJsonBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Fetch the value of the argument by name and set it to the model state
string fieldName = bindingContext.FieldName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
if(valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);
// Do nothing if the value is null or empty
string value = valueProviderResult.FirstValue;
if(string.IsNullOrEmpty(value)) return Task.CompletedTask;
try
{
// Deserialize the provided value and set the binding result
object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(result);
}
catch(JsonException)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
You can then use the ModelBinder attribute in your DTO class to indicate that this binder should be used to bind the MyJson property:
public class UploadPayload
{
public IFormFile File { get; set; }
[Required]
[StringLength(32)]
[FromForm]
public string Md5 { get; set; }
[ModelBinder(BinderType = typeof(FormDataJsonBinder))]
public List<string> SessionIds { get; set; }
}
You can read more about custom model binding in the ASP.NET Core documentation: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
I'm not 100% clear on how this would work for ASP.NET Core but for Web API (so I assume a similar path exists here) you'd want to go down the road of a Media Formatter. Here's an example (fairly similar to your question) Github Sample with blog post
Custom formatters might be the ticket? https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters

POST controller value is always NULL

Firstly thank you for taking the time out to read this post.
I've been building my first asp.net 4.5 MVC4 Web Api, but the value received in the controll POST method is always NULL.
To test the POST api method I've tried Fiddler and Google Chrome Simple REST Clinet but the result is always the same.
Here's my code:
Controller (POST)
// POST api/Terminal
public HttpResponseMessage PostTerminal(Terminal terminal)
{
if (terminal == null)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, terminal);
}
if (ModelState.IsValid)
{
db.Terminals.Add(terminal);
db.SaveChanges();
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, terminal);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = terminal.Id }));
return response;
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
}
Model Class
public class Terminal
{
public int Id { get; set; }
[Required]
public int TerminalId { get; set; }
[Required]
public string Tag { get; set; }
[Required]
public DateTime TagDate { get; set; }
}
In the above code, the value received in the POST method terminal is always NULL.
Here's the command I've been using to POST using Fiddler:
Method:
POST
Address:
http://localhost:52036/api/terminal
Header:
Content-Type: application/json
Host: localhost:52036
Content-Length: 60
Body:
{"TerminalId":123,"Tag":"222","TagDate":2014-04-13 04:22:12}
----------- EDIT - 13-04-2014 14:56 -------------
I have modified my Fiddler http post request as advised to in some of the replies to this question with the following details. However I keep getting a 500 HTTP error and my API code doesn't even reach the Controller when testing in VS2013 debug mode.
Address
POST
http://localhost:52036/api/terminal/
Header
Content-Type: application/json; charset=utf-8
Host: localhost:52036
Content-Length: 62
Body
{"TerminalId":123,"Tag":"222","TagDate":"2014-04-13 04:22:12"}
-------------- Edit 13/04/2014 20:38 -----------------
Ok, using Google's Chrome REST Client, I've noticed the following return error message:
{"Message":"An error has occurred.","ExceptionMessage":"Property
'TerminalId' on type 'ClockingDemo.Models.Terminal' is invalid.
Value-typed properties marked as [Required] must also be marked with
[DataMember(IsRequired=true)] to be recognized as required. Consider
attributing the declaring type with [DataContract] and the property
with
[DataMember(IsRequired=true)].","ExceptionType":"System.InvalidOperationException","StackTrace":"
at
System.Web.Http.Validation.Validators.ErrorModelValidator.Validate(ModelMetadata
metadata, Object container)\r\n at
System.Web.Http.Validation.DefaultBodyModelValidator.ShallowValidate(ModelMetadata
metadata, ValidationContext validationContext, Object container)\r\n
at
System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(ModelMetadata
metadata, ValidationContext validationContext, Object container)\r\n
at
System.Web.Http.Validation.DefaultBodyModelValidator.ValidateProperties(ModelMetadata
metadata, ValidationContext validationContext)\r\n at
System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(ModelMetadata
metadata, ValidationContext validationContext, Object container)\r\n
at
System.Web.Http.Validation.DefaultBodyModelValidator.Validate(Object
model, Type type, ModelMetadataProvider metadataProvider,
HttpActionContext actionContext, String keyPrefix)\r\n at
System.Web.Http.ModelBinding.FormatterParameterBinding.<>c__DisplayClass1.b__0(Object
model)\r\n at
System.Threading.Tasks.TaskHelpersExtensions.<>c__DisplayClass361.<>c__DisplayClass38.<Then>b__35()\r\n
at
System.Threading.Tasks.TaskHelpersExtensions.<>c__DisplayClass49.<ToAsyncVoidTask>b__48()\r\n
at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func1
func, CancellationToken cancellationToken)"}
I think the problem is coming from the fact that my INTEGER property TerminalId is defined as [Required]. I have a feeling there might be an issue with setting a non-NULLABLE property and [Required].
Very much open to suggestions.
----------------- ANSWER ---------------
I finally stumbled across this thread which solved the problem for me.
DataAnnotation for Required property
Simply paste this into the Global.asax file:
GlobalConfiguration.Configuration.Services.RemoveAll(
typeof(System.Web.Http.Validation.ModelValidatorProvider),
v => v is InvalidModelValidatorProvider);
I'm still open to other solutions if you believe there is a better method.
In Fiddler you have to use like below
Your Request Body should like this {"TerminalId":123,"Tag":"222","TagDate":"2014-04-13 04:22:12"}
and in your request header you need to pass content type like this Content-Type: application/json; charset=utf-8
In my case I have a hybrid ASP.NET Web Forms with MVC and Web API.
Just solved like this:
public HttpResponseMessage PostTerminal(JObject jsonMessage)
{
var terminal = jsonMessage.ToObject<Terminal>();
if (terminal == null)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, terminal);
}
...
}
References: http://weblog.west-wind.com/posts/2012/May/08/Passing-multiple-POST-parameters-to-Web-API-Controller-Methods

Return a model containing stream in ASP MVC4 Webapi

My model class looks like this
public Stream FileStream { get; set; }
public string FileName { get; set; }
public string ContentType { get; set; }
Is it possible to return this model to webapi?
A Stream is just a pointer to some actual resource. If you want to send this to the response and be able to serialize it you could use a byte array.
Or event better, write the Stream to the response and then use standard HTTP headers for the 2 other properties:
Content-Type: application/pdf
Content-Disposition: attachment; filename=report.pdf
... the actual content comes here
You could also consider writing a custom MediaTypeFormatter to achieve this as shown in this article.
No. This cannot be automatically serialised. But returning a stream is easy. See here.

Fiddler - Change HTTP Request Header

I wish to create a HTTP request header using Fiddler.
I have a Service running which exposes a Method, which has an object parameter. The object looks like this:
[DataMember]
public string Name { get; set; }
[DataMember]
public string Data { get; set; }
[DataMember]
public string Details { get; set; }
....
Does anyone know how I can populate these objects and send them to my WCFServices? I am using localhost.
Solved by:
Opening Fiddler, clicking on Composer, adding:
Content-Type: application/json; charset=utf-8 In the Parsed tab.
And Key pair values in the Request Body, e.g:
{
"Name" : "Arnold",
"Data" : "SomeDataHere"
}
how I can populate these objects
Take a look at SOAP UI or at the WCF Test Client.

ASP.Net Web Api not binding model on POST

I'm trying to POST JSON data to a Web Api method but the JSON data is not binding to the model.
Here's my model:
[DataContract]
public class RegisterDataModel
{
[DataMember(IsRequired = true)]
public String SiteKey { get; set; }
[DataMember(IsRequired = true)]
public String UserId { get; set; }
[DataMember(IsRequired = true)]
public String UserName { get; set; }
}
Here's my Web Api action:
public class RegisterController : ApiController
{
public Guid Post([ModelBinder] RegisterDataModel registerDataModel)
{
if (!ModelState.IsValid)
{
throw new ModelStateApiException(ModelState);
}
var userProfileDataContract = userProfileBusinessLibrary.GetNewOne();
userProfileDataContract.UserId = registerDataModel.UserId;
userProfileDataContract.UserName = registerDataModel.UserName;
var userKey = userProfileBusinessLibrary.Register(registerDataModel.SiteKey, userProfileDataContract);
return userKey;
}
}
Before I added [ModelBinder], registerDataModel was null. After adding [ModelBinder], registerDataModel is a RegisterDataModel instance, but all of the property values are null.
Here's my Request via Fiddler:
http://local.testwebsite.com/api/register
Request Headers:
User-Agent: Fiddler
Host: local.testwebsite.com
Content-Length: 89
Content-Type: application/json; charset=utf-8:
Request Body:
{
"SiteKey":"qwerty",
"UserId": "12345qwerty",
"UserName":"john q"
}
What am I missing to make my post data bind to the RegisterDataModel properties? Thanks for your help.
Not related to the OP's problem, but the title of the question
led me here when I used (public) fields instead of properties
in the Model class (i.e. no {get; set;}).
It turned out that this also causes the binding to fail.
Maybe helps someone.
How are you creating the JSON request? Through Fiddler request builder? Try just the following in the request body.
{
"SiteKey":"qwerty",
"UserId": "12345qwerty",
"UserName":"john q"
}
I'm guessing 'Request Body:' is also part of your request body. Remove that and check.
In my case, app's requests are passed through a middleware called "API Manager" for authentication / authorization before forwarding to my .NET Web API. POST parameter isn't binded because, for some reason I'm no idea why, the "Content-Length" is emitted from the Headers.The reason is because, the default JsonMediaTypeFormatter always check requests' Content-Length before doing model binding, and if the Content-Length is not presented it will set the parameter to NULL.