Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapt .MultiPart to support enhanced file upload #110

Closed
RipaBogdan opened this issue Nov 2, 2023 · 20 comments
Closed

Adapt .MultiPart to support enhanced file upload #110

RipaBogdan opened this issue Nov 2, 2023 · 20 comments
Assignees
Labels
enhancement New feature or request waiting for feedback Waiting for feedback from issue submitter

Comments

@RipaBogdan
Copy link

RipaBogdan commented Nov 2, 2023

Can .MultiPart be addapted in order to allow dictionaries as input?

There are situations where you need to upload a file and some additional details are required.

Current implementation:
MultiPart(FileInfo fileName, string controlName="file")

Would maybe require to accept a dictionary of type <string, object>, eg:

multiPart = new Dictionary<string, object>
 {
     {"projectId", "id"},
     { "type", "Model"}
     { "file", fileName }
 };
@basdijkstra
Copy link
Owner

Hey @RipaBogdan, sure, I'm happy to see if I can support that. To make it a little easier for me to implement, do you maybe have an example of what the final request should look like? Ideally sourced from a tool like Fiddler or WireShark.

Once I know that I'll think of a syntax that would support it, show that to you and if you're happy with it I'm happy to build in support for it.

@basdijkstra basdijkstra self-assigned this Nov 2, 2023
@basdijkstra basdijkstra added the enhancement New feature or request label Nov 2, 2023
@basdijkstra basdijkstra changed the title Addapt .MultiPart for enhanced file upload Adapt .MultiPart to support enhanced file upload Nov 2, 2023
@RipaBogdan
Copy link
Author

Hi @basdijkstra, unfortunately I don't use these tools, however I can provide this as an example of what I'm talking about:
image
The idea is to have the file as part of some dictionary structure.
Maybe .FormData should instead be the one to be enhanced?

@basdijkstra
Copy link
Owner

Thanks for that. I'll see if I can find some more information on this.

By the way, it looks like this request is generated when some kind of web form is submitted. You wouldn't by any chance have a public example of such a form available for me to use / test, would you?

@basdijkstra
Copy link
Owner

@RipaBogdan do you have an example of such a form for me? Without a way to see what the request should look like it's going to be a little hard to implement this as I can't write tests for it.

@RipaBogdan
Copy link
Author

unfortunately the form data above is all I have as an example, but the idea remains the same, we need a way to upload a file along with other fields.

@basdijkstra
Copy link
Owner

Got it. I’ll do some more research and will try to provide you with a test version soon.

@basdijkstra
Copy link
Owner

I think I'm getting somewhere here, but it does require me to rewrite some of the internals of how a request body is created.

It would be an improvement, though, so I'm happy to do it, but it might take me a bit more time. Will keep you posted, @RipaBogdan.

@basdijkstra
Copy link
Owner

@RipaBogdan can you tell me if there is a wrapper around the various elements in the screenshot you attached in #110 (comment) ?

This is what I've got now:
image
As you can see, there's an outer boundary 327d46c5-c7d6-481a-bf38-c7dff4a8f6ab containing (right now) one nested element with the inner boundary 0b12acc5-347d-4b2f-98bf-fb0af5d696e1.

I could probably very easily allow for more inner sections to be added here, and I think this is the way to do it as a HttpMessage only has a singular Content property of type HttpContent, no IEnumerable, so it has to be any of these:

System.Net.Http.ByteArrayContent
System.Net.Http.Json.JsonContent
System.Net.Http.MultipartContent
System.Net.Http.ReadOnlyMemoryContent
System.Net.Http.StreamContent

And the MultiPartContent can have multiple segments like in your example.

So, again, is there are wrapper around the payload sections you showed in that screenshot? Can I see the entire payload?

@basdijkstra
Copy link
Owner

Note to self: I think this is the way to do it: https://brokul.dev/sending-files-and-additional-data-using-httpclient-in-net-core. Let me try and implement this over the weekend.

@basdijkstra
Copy link
Owner

Got it:
image

Now all I need to do is think about what the methods should look like.

Right now, I simply added a method MultiPart(HttpContent content, string name) , but if you have to send many different fields, that might get ugly. It works, though, so this option will definitely be in there.

@basdijkstra
Copy link
Owner

basdijkstra commented Nov 18, 2023

Well, that was easier than expected.

[Test]
public void AdditionalFieldsCanBeUploadedWithAFileUsingADictionary()
{
    Dictionary<string, HttpContent> additionalMultipartPayload = new Dictionary<string, HttpContent>()
    {
        { "projectId", new StringContent("PROJECT-1234") },
        { "projectName", new StringContent("MyProject") },
    };

    Given()
        .MultiPart(new FileInfo(this.csvFileName))
        .MultiPart(additionalMultipartPayload)
    .When()
        .Post($"{MOCK_SERVER_BASE_URL}/csv-multipart-form-data-additional-fields")
    .Then()
        .StatusCode(201);
}

MultiPart() now also accepts an argument of type Dictionary<string, HttpContent>. That means you can add as many fields as you want with a single call.

You can even add the file itself through the Dictionary, too, but you'll need to convert it to an object of type HttpContent yourself. That's not too difficult, though, you can do that like this:

Dictionary<string, HttpContent> multipartPayloadIncludingFile = new Dictionary<string, HttpContent>()
{
    { "file", new StreamContent(fileInfo.OpenRead()) },
    { "projectId", new StringContent("PROJECT-1234") },
    { "projectName", new StringContent("MyProject") },
};

where fileInfo is the FileInfo object used before.

@RipaBogdan what do you think? I've just published a new beta version so you can try it out for yourself, it's 4.2.0-beta.3.

Please let me know what you think once you've tested it so I can release a proper 4.2.0 version soon (I've been planning to do this for a while).

@basdijkstra basdijkstra added the waiting for feedback Waiting for feedback from issue submitter label Nov 18, 2023
@RipaBogdan
Copy link
Author

@basdijkstra I have tested .MultiPart and can confirm that this solution is working as expected.
Thank you!

@basdijkstra
Copy link
Owner

Awesome, thank you @RipaBogdan for your support. I'll release version 4.2.0 later this week, I want to do some final testing tomorrow (around .NET 8 support, not this feature).

Please note that I did change the order of arguments for one of the MultiPart() methods, from Multipart(HttpContent content, string name) to MultiPart(string name, HttpContent content) so it matches the order that the dictionary argument takes.

@basdijkstra
Copy link
Owner

Just a quick heads up in case you missed it, @RipaBogdan: I released version 4.2.0 last week, including this feature.

Thanks again and let me know if there’s anything else you think is missing or misbehaving!

@RipaBogdan
Copy link
Author

Hi @basdijkstra, there is a small thing that is giving us trouble regarding this .MultiPart file upload, that maybe can be something quick from your side.
As you know, endpoints use various names for multipart/formdata fields, and it seems that .MultiPart automatically adds the name="file" to the field we use for uploading something.
image

I suspect this is causing our issues, since our endpoint looks for name="content".
Is it possible to maybe add the "name" to be filled in by the user?

Thank you,
Bogdan

@RipaBogdan RipaBogdan reopened this Jan 4, 2024
@basdijkstra
Copy link
Owner

Hey @RipaBogdan, the old way of specifying a multipart file upload with the option to specify a custom control name is still available, would that work for you in this case?

See

public void MultiPartFormDataWithCustomControlNameAndCustomContentTypeCanBeSupplied()

@RipaBogdan
Copy link
Author

RipaBogdan commented Jan 5, 2024

Unfortunately, it seems that we need a mix between the two,
So something like this maybe, if it is possible:

`[Test]
public void AdditionalFieldsCanBeUploadedWithAFileUsingADictionary()
{
    Dictionary<string, HttpContent> additionalMultipartPayload = new Dictionary<string, HttpContent>()
    {
        { "projectId", new StringContent("PROJECT-1234") },
        { "projectName", new StringContent("MyProject") },
    };

    Given()
        **.MultiPart("Content", new FileInfo(this.csvFileName))**
        .MultiPart(additionalMultipartPayload)
    .When()
        .Post($"{MOCK_SERVER_BASE_URL}/csv-multipart-form-data-additional-fields")
    .Then()
        .StatusCode(201);
}`

@basdijkstra
Copy link
Owner

Have you tried doing just that? Because the way it’s implemented each additional call to MultiPart adds another section to the multipart content:

public ExecutableRequest MultiPart(FileInfo fileName, string controlName = "file", MediaTypeHeaderValue? contentType = null)

I even have a test for it myself:

public void AdditionalFieldsCanBeUploadedWithAFileAsIndividualComponents()

If it doesn’t work as you expected my tests aren’t doing what they should :)

@RipaBogdan
Copy link
Author

Indeed, it works, just needed to switch the parameters around:

.MultiPart(new FileInfo(csvFileName), "content")

Thank you!

@basdijkstra
Copy link
Owner

No problem, you’re welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request waiting for feedback Waiting for feedback from issue submitter
Projects
None yet
Development

No branches or pull requests

2 participants