16

I'm trying to upload a file with body content. Is PostMultipartAsync the only way?

On my C# backend code I have this:

var resource = FormBind<StorageFileResource>();
var file = Request.Files.First().ToPostedFile();

FormBind reads data from the request and fills the object.

By using PostMultipartAsync I know it should start like this:

.PostMultipartAsync((mp) => { mp.AddFile(name, stream, name)}), but I can't figure out how to add the object. Do you have any ideas on that?

This is my current try:

public static async Task<T> PostFileAsync<T>(string url, object data, string name, Stream stream, object queryString = null)
    where T : class
{
    return await HandleRequest(async () => queryString != null
        ? await url
            .SetQueryParams(queryString)
            .SetClaimsToken()
            .PostMultipartAsync((mp) => { mp.AddFile(name, stream, name)})
            .ReceiveJson<T>()
        : await url
            .SetClaimsToken()
            .PostMultipartAsync((mp) => mp.AddFile(name, stream, name))
            .ReceiveJson<T>());
}

Current request being made by the front end:

enter image description here

eestein
  • 4,914
  • 8
  • 54
  • 93
  • Does `ToPostedFile` return a [System.Web.HttpPostedFile](https://msdn.microsoft.com/en-us/library/system.web.httppostedfile.aspx)? Also, what is `resource` and how it it relevant here? `file` should contain everything you want to post, no? – Todd Menier Dec 08 '16 at 16:32
  • Also, regarding "is it the only way" to upload a file with Flurl: Technically no, but the API or web service you're calling should define the format it requires, be it `multipart/form-data` or something else. That's the important part to find out first. – Todd Menier Dec 08 '16 at 16:36
  • @ToddMenier Hi Todd, regarding the first question, no, it's a local class. I read from the `HttpFile`'s list. Resource is my ViewModel class, I'm only binding the request fields with that class, it's purpose here is to show I'm reading the other fields separately. As to the 3rd part, it is an API that requires `multipart/form-data`, please check my updated answer to see how it's being done today using the frontend. – eestein Dec 08 '16 at 17:12
  • Are you asking how to do a multipart post that requires both a file and a JSON object? – Todd Menier Dec 08 '16 at 18:41
  • @ToddMenier yup :) – eestein Dec 09 '16 at 12:47
  • @ToddMenier to expand on that... That request on my question is the one the web app makes today. I'm using flurl on my Xamarin.iOS app. So I need to make the same request to the API. Thanks again! – eestein Dec 09 '16 at 13:48

2 Answers2

34

There are a variety of ways to add "parts" to a multipart POST with Flurl. I haven't added this to the docs yet but here's an example from the issue that basically demonstrates every possibility:

var resp = await "http://api.com"
    .PostMultipartAsync(mp => mp
        .AddString("name", "hello!")                // individual string
        .AddStringParts(new {a = 1, b = 2})         // multiple strings
        .AddFile("file1", path1)                    // local file path
        .AddFile("file2", stream, "foo.txt")        // file stream
        .AddJson("json", new { foo = "x" })         // json
        .AddUrlEncoded("urlEnc", new { bar = "y" }) // URL-encoded                      
        .Add(content));                             // any HttpContent
Todd Menier
  • 37,557
  • 17
  • 150
  • 173
  • Thanks! I had to use `AddJson` for each property, since I need it in the Form's request body, by only using `AddJson("json",data)` the object is serialized inside the json property, inside the Form property. Thank you :) – eestein Dec 09 '16 at 16:30
  • I am trying to send nested properties via `AddStringParts` method, but it generates `dynamic.ToString()` which is invalid json. I just need only flattening nested properties by seperating dots. ` new { User = new { Name = "Ahmed" } }` => `User.Name="Ahmed"` – guneysus Feb 03 '17 at 08:53
  • 1
    @guneysus I don't think that's a very common scenario and Flurl does not have direct support for it. You'd need to sort of pick it apart yourself and do `.AddString("User.Name", "Ahmed")`. (As a side-note, this scenario has nothing to do with JSON. `User={"Name":"Ahmed"}` is how you'd pass this as a JSON-encoded value, which is much more common.) – Todd Menier Feb 05 '17 at 15:36
  • Is there a way to do this with the 'PUT' verb? The API I am using supports only a 'PUT' for the multi-part operation. This is because the API reads data from the file that is send and does a create or update with the content that was read. I am going to do a PR that will add support for it. I really want to use Flurl in this case and not HttpClient as Flurl is much cleaner. – Rob L Aug 03 '18 at 10:22
  • @RobL That's not very common so there isn't an extension method for it out of the box, but writing your own is about a 3-liner. Please ask in a new question and I'll provide the code. – Todd Menier Aug 12 '18 at 13:56
  • 1
    I figured it out. Thanks. – Rob L Aug 13 '18 at 09:15
  • 1
    I would appreciate if you could also show how to assert this multipart call within flurl. Specifically, if a request contained given part and a content. – Valera Jul 27 '20 at 18:32
  • How can I loop trough my `Dictionary` and add values to the `AddStringParts` or `AddString`? – Rumplin Nov 21 '20 at 20:44
1

Here is one way that works for me

var result = await endPointApi
    .AppendPathSegments("api","AppFileManager") 
    .WithOAuthBearerToken(token.AccessToken)
    .PostMultipartAsync(mp => mp
        //.AddFile("UploadFile", @"C:\Users\..\Documents\upload.txt")
        .AddFile("UploadFile", new MemoryStream(data), appFile.FileName)
        .AddStringParts(new 
        { 
            IRN = appFile.IRN,
            TransactionIRN = appFile.TransactionIRN, 
            FileName = appFile.FileName,
            TableName = appFile.TableName,
            FileExtension = appFile.FileExtension,
        })

Web Api Controller Implementation (using MediatR)

[HttpPost]
public async Task<IActionResult> Post([FromForm] AppFileManagerCommands.Upload uploadAttachment)
{
  await mediator.Send(uploadAttachment);
  return NoContent(); 
}