restsharp / RestSharp

Simple REST and HTTP API Client for .NET
https://restsharp.dev
Apache License 2.0
9.57k stars 2.34k forks source link

Make parameters order in a post query configurable #2098

Open Ducatel opened 1 year ago

Ducatel commented 1 year ago

Hi,

has already mention in this issue #1937 I create this issue to speak about technical solution I will PR.

Is your feature request related to a problem? Please describe.

Some API require an exact order of fields in POST multipart formdata. I the actual behavior the file is always put in first.

Describe the solution you'd like Make fields order parametable for each request.

To do that, I plan to add in RestRequest an array of 3 Enum value (like ParameterType.File, ParameterType.Body, ParameterType.Post ) which will define the order. (Maybe the enum already exist ? I don't really search for yet ) The default order will be the actual one [File, body, post] to not break existing code.

In the RequestContent.BuildContent I will use this array to re-order call from line 46 to 53 (maybe will add headers in the list also, not sure yet)

Do you thinks this can be a good solution ? Or do you have better idea ?

Describe alternatives you've considered No work around found

alexeyzimarev commented 1 year ago

It can be done that parameters are added to the request in the same order as they are added to RestRequest. Parameter types already exist. It is a bit unclear from the issue description if it's about query parameters alone (they should be already added in the same order as supplied), or any parameter. I remember issues asking for parameters order in multipart forms.

The cross-type parameter order is an issue with current implementation as RestSharp adds them by parameter type (all POST parameters, all files, etc), so order between parameter types is not kept. It can be solved, but it means that the request builder needs to be rebuilt.

Ducatel commented 1 year ago

The cross-type parameter order is an issue with current implementation as RestSharp adds them by parameter type (all POST parameters, all files, etc), so order between parameter types is not kept. It can be solved, but it means that the request builder needs to be rebuilt.

It's exactly that. In your case we need to have files at the end and POST parameters in first places. The solution I explain allow to solve my current issue, but not mixing doing stuff like

To do that, we indeed have to rebuild the query builder or allow to use a different query builder. Maybe an IQueryBuilder interface which expose BuildContent method. Or allow the usage of child class from RequestContent which allow overriding BuildContent

alexeyzimarev commented 1 year ago

I think the solution should be to do that rewrite I mentioned.

paul-sh commented 9 months ago

Here's my use case for the reference: the web server requires files to go after other parameters in multipart POST requests. Meanwhile, RestSharp 110.2.0 adds files first. My workaround is to reorder content manually in OnBeforeRequest. The OrderBy method uses stable sort, so the relative order between other parameters is preserved.

request.OnBeforeRequest += message =>
{
    if (message.Content is MultipartFormDataContent oldContent)
    {
        var newContent = new MultipartFormDataContent(
            oldContent.Headers.ContentType?.Parameters?.FirstOrDefault(p => p.Name == "boundary")?.Value
            ?? "---" + Guid.NewGuid().ToString("N"));
            foreach (var c in oldContent.OrderBy(content => content is StreamContent ? 1 : 0))
            {
                newContent.Add(c);
            }
            message.Content = newContent;
    }
    return ValueTask.CompletedTask;
};
b166er commented 9 months ago

if it helps anyone, I was able to find a solution as follows. In my API requirements, there must be some JSON metadata at the first part of the multipart message, and only then the binary file part. I had previously added the metadata as a JSON string in ParameterType.Body, and then the file, so I had to organize the order myself (see 2115#issuecomment-1636157099) now I have converted the metadata json string into a jsonobject and passed it as a separate file with ParameterType.File. So I end up with ONLY multiple ParameterType.File that I can add in my specific order because I don't include mixed paramater types.

Grueslayer commented 3 months ago

I've taken @paul-sh 's approach and made a hack to provide a solution for @Ducatel 's problem (while @b166er 's way can fix most APIs)

You need to use my AddFileAtIndex(...) methods instead of the original AddFile(...) ones, like

            request.AddParameter("Payload1", "{ \"x\" : \"y\" }");
            request.AddFileAtIndex("File1", new byte[3] { 1, 2, 3 }, "xyz1.pdf");
            request.AddFileAtIndex("File2", new byte[3] { 1, 2, 3 }, "xyz2.pdf");
            request.AddParameter("Payload2", "{ \"x\" : \"y\" }");
            request.AddFileAtIndex("File3", new byte[3] { 1, 2, 3 }, "xyz3.pdf");
            request.AddHeader("Content-Type", "multipart/form-data");
            request.AlwaysMultipartFormData = true;

The implementation is done in a class extension for RestRequest:

namespace RestSharp
{
    public static partial class RestRequestFileIndexExtensions
    {
        public const string FileIndexParameterNamePrefix = "$RestSharpAddFile_";

        public static RestRequest AddFileAtIndex(
            this RestRequest request,
            string name,
            string path,
            ContentType contentType = null,
            FileParameterOptions options = null
        )
        {
            request = request.AddFile(name, path, contentType, options);
            request = request.AddParameter(FileIndexParameterNamePrefix + (request.Files.Count - 1).ToString(CultureInfo.InvariantCulture), @"");
            request.OnBeforeRequest -= OnBeforeRequestAddFileIndex;
            request.OnBeforeRequest += OnBeforeRequestAddFileIndex;
            return request;
        }

        public static RestRequest AddFileAtIndex(
            this RestRequest request,
            string name,
            byte[] bytes,
            string fileName,
            ContentType contentType = null,
            FileParameterOptions options = null
        )
        {
            request = request.AddFile(name, bytes, fileName, contentType, options);
            request = request.AddParameter(FileIndexParameterNamePrefix + (request.Files.Count - 1).ToString(CultureInfo.InvariantCulture), @"");
            request.OnBeforeRequest -= OnBeforeRequestAddFileIndex;
            request.OnBeforeRequest += OnBeforeRequestAddFileIndex;
            return request;
        }

        public static RestRequest AddFileAtIndex(
            this RestRequest request,
            string name,
            Func<Stream> getFile,
            string fileName,
            ContentType contentType = null,
            FileParameterOptions options = null
        )
        {
            request = request.AddFile(name, getFile, fileName, contentType, options);
            request = request.AddParameter(FileIndexParameterNamePrefix + (request.Files.Count - 1).ToString(CultureInfo.InvariantCulture), @"");
            request.OnBeforeRequest -= OnBeforeRequestAddFileIndex;
            request.OnBeforeRequest += OnBeforeRequestAddFileIndex;
            return request;
        }

        public static Func<HttpRequestMessage, ValueTask> OnBeforeRequestAddFileIndex = message =>
        {
            if (message.Content is MultipartFormDataContent oldContent)
            {
                var newContent = new MultipartFormDataContent(
                oldContent.Headers.ContentType?.Parameters?.FirstOrDefault(p => p.Name == "boundary")?.Value?.Trim('"')
                    ?? "---" + Guid.NewGuid().ToString("N"));
                var streamContentList = oldContent.Where(content => content is StreamContent).ToList();
                foreach (var c in oldContent.Where(content => !(content is StreamContent)))
                {
                    if (c.Headers.ContentDisposition.Name.StartsWith(FileIndexParameterNamePrefix))
                    {
                        int index = int.Parse(c.Headers.ContentDisposition.Name.Substring(FileIndexParameterNamePrefix.Length), CultureInfo.InvariantCulture);
                        newContent.Add(streamContentList[index]);
                    }
                    else
                    {
                        newContent.Add(c);
                    }
                }
                message.Content = newContent;
            }
            return new ValueTask(Task.Delay(0));
        };
    }
} 
alexeyzimarev commented 3 months ago

I believe it's all fixable in RestSharp, but it requires a complete rewrite of the RequestContent class. Unfortunately, I don't have time for that right now.