RESTful API requests using C# for MS Windows

HTTP request C# Windows

In a recent article we showed how HTTP requests are formed in low level. We demonstrated how to create a simple HTTP GET request, a URL encoded POST request, and an upload request (multipart POST HTTP request).

This article has working examples on how to create those HTTP requests programmatically using C# for MS Windows.

The next sections of this article include the code that does the job, and then a couple of examples that demonstrate ways to create different types of HTTP requests.

The code

Create a new class (Menu: Project > Add Class..) with any name and copy/paste the following code. The file name in our example is HttpRequestWorker.cs . Adjust the namespace to match the one used by your project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Net;
using System.IO;

namespace LowLevelHttpRequest {

    enum HttpRequestVarLayout {NOT_SET, ADDRESS, URL_ENCODED, MULTIPART};

    class HttpRequestInputFileElement {

        public String variable_name;
        public String local_filename;
        public String request_filename;
        public String mime_type;

    }

    class HttpRequestInput {

        public String url_str;
        public String http_method;
        public HttpRequestVarLayout var_layout = HttpRequestVarLayout.NOT_SET;
        public Dictionary<String, String> vars;
        public List<HttpRequestInputFileElement> files;

        public HttpRequestInput() {
            initialize();
        }

        public HttpRequestInput(String v_url_str, String v_http_method) {
            initialize();
            url_str = v_url_str;
            http_method = v_http_method;
        }

        private void initialize() {
            url_str = "";
            http_method = "GET";
            vars = new Dictionary<String, String>();
            files = new List<HttpRequestInputFileElement>();
        }

        public void add_var(String key, String value) {
            vars.Add(key, value);
        }

        public void add_file(String variable_name, String local_filename, String request_filename, String mime_type) {
            HttpRequestInputFileElement file = new HttpRequestInputFileElement();
            file.variable_name = variable_name;
            file.local_filename = local_filename;
            file.request_filename = request_filename;
            file.mime_type = mime_type;
            files.Add(file);
        }

    }

    class HttpRequestWorker {

        public StringBuilder response;
        public Exception error;
        public event EventHandler OnCompleted;

        const int BUFFER_SIZE = 4096;
        public byte[] buffer;
        public HttpWebRequest request;
        public Stream stream_response;
        private List<byte[]> request_content;

        public void add_response_content(int length) {
            response.Append(Encoding.ASCII.GetString(buffer, 0, length));
        }

        public void execution_completed() {
            if (stream_response != null) {
                stream_response.Close();
                stream_response = null;
            }
            OnCompleted(this, EventArgs.Empty);
        }

        private StringBuilder urlencode(String input) {
            byte[] input_c = Encoding.UTF8.GetBytes(input);
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < input_c.Length; i++) {
                char c = (char)input_c[i];
                if (
                    (c >= '0' && c <= '9')
                    || (c >= 'A' && c <= 'Z')
                    || (c >= 'a' && c <= 'z')
                    || c == '-' || c == '.' || c == '_' || c == '~'
                ) {
                    result.Append(c);
                }
                else {
                    result.Append('%');
                    result.Append(((int)c).ToString("X2"));
                }
            }

            return result;
        }

        private String http_attribute_encode(String attribute_name, String input) {
            // result structure follows RFC 5987
            bool need_utf_encoding = false;
            StringBuilder result = new StringBuilder();
            byte[] input_c = Encoding.UTF8.GetBytes(input);
            char c;
            for (int i = 0; i < input_c.Length; i++) {
                c = (char)input_c[i];
                if (c == '\\' || c == '/' || c == '\0' || c < ' ' || c > '~') {
                    // ignore and request utf-8 version
                    need_utf_encoding = true;
                }
                else if (c == '"') {
                    result.Append("\\\"");
                }
                else {
                    result.Append(c);
                }
            }

            if (result.Length == 0) {
                need_utf_encoding = true;
            }

            if (!need_utf_encoding) {
                // return simple version
                return String.Format("{0}=\"{1}\"", attribute_name, result);
            }

            StringBuilder result_utf8 = new StringBuilder();
            for (int i = 0; i < input_c.Length; i++) {
                c = (char)input_c[i];
                if (
                    (c >= '0' && c <= '9')
                    || (c >= 'A' && c <= 'Z')
                    || (c >= 'a' && c <= 'z')
                ) {
                    result_utf8.Append(c);
                }
                else {
                    result_utf8.Append('%');
                    result_utf8.Append(((int)c).ToString("X2"));
                }
            }

            // return enhanced version with UTF-8 support
            return String.Format("{0}=\"{1}\"; {0}*=utf-8''{2}", attribute_name, result, result_utf8);
        }

        private void add_request_content(String str) {
            request_content.Add(Encoding.UTF8.GetBytes(str));
        }

        private void add_request_content(StringBuilder str) {
            request_content.Add(Encoding.UTF8.GetBytes(str.ToString()));
        }

        private void add_request_content(char c) {
            request_content.Add(new byte[1] {(byte)c});
        }

        public void execute(HttpRequestInput input) {
            try {
                // reset variables

                response = new StringBuilder();
                error = null;

                buffer = new byte[BUFFER_SIZE];
                stream_response = null;
                request_content = new List<byte[]>();


                // decide on the variable layout

                if (input.files.Count > 0) {
                    input.var_layout = HttpRequestVarLayout.MULTIPART;
                }
                if (input.var_layout == HttpRequestVarLayout.NOT_SET) {
                    input.var_layout = input.http_method.Equals("GET") || input.http_method.Equals("HEAD")
                        ? HttpRequestVarLayout.ADDRESS
                        : HttpRequestVarLayout.URL_ENCODED;
                }


                // prepare request content

                String boundary = "";

                if (input.var_layout == HttpRequestVarLayout.ADDRESS || input.var_layout == HttpRequestVarLayout.URL_ENCODED) {
                    // variable layout is ADDRESS or URL_ENCODED

                    if (input.vars.Count > 0) {
                        bool first = true;
                        foreach (KeyValuePair<string, string> element in input.vars) {
                            if (!first) {
                                add_request_content('&');
                            }
                            first = false;

                            add_request_content(urlencode(element.Key));
                            add_request_content('=');
                            add_request_content(urlencode(element.Value));
                        }

                        if (input.var_layout == HttpRequestVarLayout.ADDRESS) {
                            StringBuilder new_url = new StringBuilder(input.url_str);
                            new_url.Append("?");

                            foreach (byte[] ba in request_content) {
                                new_url.Append(Encoding.UTF8.GetString(ba));
                            }

                            input.url_str = new_url.ToString();
                            request_content.Clear();
                        }
                    }
                }
                else {
                    // variable layout is MULTIPART

                    Random random = new Random();
                    int boundary_random = random.Next();
                    int boundary_timestamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;

                    boundary = "__-----------------------" + boundary_random.ToString() + boundary_timestamp.ToString();
                    String boundary_delimiter = "--";
                    String new_line = "\r\n";

                    // add variables
                    foreach (KeyValuePair<string, string> element in input.vars) {
                        // add boundary
                        add_request_content(boundary_delimiter);
                        add_request_content(boundary);
                        add_request_content(new_line);

                        // add header
                        add_request_content("Content-Disposition: form-data; ");
                        add_request_content(http_attribute_encode("name", element.Key));
                        add_request_content(new_line);
                        add_request_content("Content-Type: text/plain");
                        add_request_content(new_line);

                        // add header to body splitter
                        add_request_content(new_line);

                        // add variable content
                        add_request_content(element.Value);
                        add_request_content(new_line);
                    }

                    // add files
                    foreach (HttpRequestInputFileElement file_info in input.files) {
                        FileInfo fi = null;
                        if (file_info.local_filename != null && file_info.local_filename.Length > 0) {
                            fi = new FileInfo(file_info.local_filename);
                        }

                        // ensure necessary variables are available
                        if (
                            file_info.variable_name == null || file_info.variable_name.Length == 0
                            || fi == null || !fi.Exists
                        ) {
                            // silent abort for the current file
                            continue;
                        }

                        FileAttributes attr = File.GetAttributes(file_info.local_filename);
                        if (
                            (attr & FileAttributes.Directory) == FileAttributes.Directory
                            || (attr & FileAttributes.Device) == FileAttributes.Device
                        ) {
                            // silent abort for the current file
                            continue;
                        }

                        // ensure filename for the request
                        if (file_info.request_filename == null || file_info.request_filename.Length == 0) {
                            file_info.request_filename = fi.Name;
                            if (file_info.request_filename.Length == 0) {
                                file_info.request_filename = "file";
                            }
                        }

                        // add boundary
                        add_request_content(boundary_delimiter);
                        add_request_content(boundary);
                        add_request_content(new_line);

                        // add header
                        add_request_content(String.Format("Content-Disposition: form-data; {0}; {1}",
                            http_attribute_encode("name", file_info.variable_name),
                            http_attribute_encode("filename", file_info.request_filename)
                        ));
                        add_request_content(new_line);

                        if (file_info.mime_type != null && file_info.mime_type.Length > 0) {
                            add_request_content("Content-Type: ");
                            add_request_content(file_info.mime_type);
                            add_request_content(new_line);
                        }

                        add_request_content("Content-Transfer-Encoding: binary");
                        add_request_content(new_line);

                        // add header to body splitter
                        add_request_content(new_line);

                        // add file content
                        request_content.Add(File.ReadAllBytes(file_info.local_filename));
                        add_request_content(new_line);
                    }

                    // add end of body
                    add_request_content(boundary_delimiter);
                    add_request_content(boundary);
                    add_request_content(boundary_delimiter);
                }


                // prepare connection

                request = (HttpWebRequest)WebRequest.Create(input.url_str);
                request.Proxy = null;
                request.Method = input.http_method;
                request.UserAgent = "Agent name goes here";

                if (input.var_layout == HttpRequestVarLayout.URL_ENCODED) {
                    request.ContentType = "application/x-www-form-urlencoded";
                }
                else if (input.var_layout == HttpRequestVarLayout.MULTIPART) {
                    request.ContentType = "multipart/form-data; boundary=" + boundary;
                }


                // send request content, if applicable

                if (input.var_layout == HttpRequestVarLayout.URL_ENCODED || input.var_layout == HttpRequestVarLayout.MULTIPART) {
                    Stream request_stream = request.GetRequestStream();
                    BinaryWriter write_stream = new BinaryWriter(request_stream);

                    foreach (byte[] ba in request_content) {
                        write_stream.Write(ba);
                    }

                    write_stream.Flush();
                    write_stream.Close();
                    request_stream.Close();
                }


                // receive response

                request.BeginGetResponse(new AsyncCallback(response_callback), this);
            }
            catch (WebException e) {
                error = e;
                execution_completed();
            }
            catch (Exception e) {
                error = e;
                execution_completed();
            }
        }

        private static void response_callback(IAsyncResult async_result) {
            HttpRequestWorker worker = (HttpRequestWorker)async_result.AsyncState;

            try {
                HttpWebResponse response = (HttpWebResponse)worker.request.EndGetResponse(async_result);
                worker.stream_response = response.GetResponseStream();
                worker.stream_response.BeginRead(worker.buffer, 0, BUFFER_SIZE, new AsyncCallback(read_callback), worker);
            }
            catch (WebException e) {
                worker.error = e;
                worker.execution_completed();
            }
            catch (Exception e) {
                worker.error = e;
                worker.execution_completed();
            }
        }

        private static void read_callback(IAsyncResult async_result) {
            HttpRequestWorker worker = (HttpRequestWorker)async_result.AsyncState;

            try {
                int length = worker.stream_response.EndRead(async_result);
                if (length > 0) {
                    worker.add_response_content(length);
                    worker.stream_response.BeginRead(worker.buffer, 0, BUFFER_SIZE, new AsyncCallback(read_callback), worker);
                }
                else {
                    worker.execution_completed();
                }
            }
            catch (WebException e) {
                worker.error = e;
                worker.execution_completed();
            }
            catch (Exception e) {
                worker.error = e;
                worker.execution_completed();
            }
        }

    }

}

The controller class should contain one or more places that call the worker unit and one handler function. The handler function is required because this example's code makes asynchronous HTTP calls in order for the main thread to keep being responsive and not freeze.

private void button1_Click(object sender, EventArgs e) {
    // trigger the request
}

private void handle_result(object sender, EventArgs e) {
    HttpRequestWorker worker = (HttpRequestWorker)sender;
    string msg;

    if (worker.error == null) {
        // communication was successful
        msg = "Success - Response: " + worker.response.ToString();
    }
    else if (worker.error is WebException) {
        // WebException exception
        msg = "WebException: " + worker.error.Message;
    }
    else if (worker.error is Exception) {
        // generic exception
        msg = "Exception: " + worker.error.Message;
    }
    else {
        // unknown response - should never happen
        msg = "Error: Unhandled communication response";
    }

    MessageBox.Show(msg);
}

There are a couple of notable things in this code:

  1. The use of asynchronous methods for the communication process.

    The experienced eye will notice the use of HttpWebRequest.BeginGetResponse() . HttpWebRequest supports several methods to communicate. We picked one that receives the web response in an asynchronous way. This helps your UI thread to keep being responsive and not freeze while the web server sends the response.

  2. The significance of specialized classes for the HTTP input.

    This code uses the class HttpRequestInput as an encapsulation for input variables of the HTTP request.

  3. The role of urlencode()

    This little function was required as C# offers this functionality only for web applications. Our example works as a desktop app and therefore is not supported by native .NET functions for URL encoding.

  4. The role of http_attribute_encode()

    One small detail is the way we must escape attribute values in the HTTP protocol. Being Greek gives us the advantage to have increased appreciation in internationalization/localization issues. When attribute values have non-latin characters things may get out of control. RFC 5987 makes the necessary provisions for not allowing the mess, and this function implements this provision.

  5. Trigger code

    The code we just showed includes the function button1_Click(). The next sections of this article will differentiate this function to implement different types of HTTP requests. In any case, this function is supposed to be an event handler of a button click from the UI.

  6. Result handling

    This code uses the handle_result() function to handle the communication result. You may notice that the result can be a number of different classes. For your convenience the one that signals success is on the top. The rest of possible cases handle different types of errors.

  7. Little things you need to adjust in your implementation:

    As previously stated, you need to adjust the namespace of the worker unit (LowLevelHttpRequest in this example) to match the one used by your project.

    This code sets the HTTP header for the name of the HTTP client/agent. You need to change "Agent name goes here" with the name of your client.

    You obviously need to set the right code for triggering the code in button1_Click() . Read the next sections for more examples on this function.

    Naturally, you should customize handle_result() . Our code is just an example of the different things you can do when the communication is completed.

Simple HTTP GET requests

Lets see how to run a simple GET request.

private void button1_Click(object sender, EventArgs e) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "GET");

    HttpRequestWorker worker = new HttpRequestWorker();
    worker.OnCompleted += handle_result;
    worker.execute(input);
}

This will produce the following HTTP request:

GET /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Host: www.example.com
Connection: Keep-Alive

The previous example calls a plain URL. Lets add a few variables.

private void button1_Click(object sender, EventArgs e) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "GET");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    HttpRequestWorker worker = new HttpRequestWorker();
    worker.OnCompleted += handle_result;
    worker.execute(input);
}

This will produce the following HTTP request:

GET /path/to/page.php?key1=value1&key2=value2 HTTP/1.1
User-Agent: Agent name goes here
Host: www.example.com
Connection: Keep-Alive

URL encoded HTTP POST requests

We can make a slight adjustment and turn the GET request to a URL encoded POST request.

private void button1_Click(object sender, EventArgs e) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "POST");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    HttpRequestWorker worker = new HttpRequestWorker();
    worker.OnCompleted += handle_result;
    worker.execute(input);
}

This will produce the following HTTP request:

POST /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Content-Type: application/x-www-form-urlencoded
Host: www.example.com
Content-Length: 23
Expect: 100-continue
Connection: Keep-Alive

key1=value1&key2=value2

Multipart HTTP POST requests

Finally, lets push it to the limits. Lets upload some files using a multipart POST request.

private void button1_Click(object sender, EventArgs e) {
    String url_str = "http://www.example.com/path/to/page.php";

    HttpRequestInput input = new HttpRequestInput(url_str, "POST");

    input.add_var("key1", "value1");
    input.add_var("key2", "value2");

    input.add_file("file1", @"C:\path\to\file1.png", null, "image/png");
    input.add_file("file2", @"C:\path\to\file2.png", null, "image/png");

    HttpRequestWorker worker = new HttpRequestWorker();
    worker.OnCompleted += handle_result;
    worker.execute(input);
}

This will produce the following HTTP request:

POST /path/to/page.php HTTP/1.1
User-Agent: Agent name goes here
Content-Type: multipart/form-data; boundary=__-----------------------9446182961397930864818
Host: www.example.com
Content-Length: 686
Expect: 100-continue
Connection: Keep-Alive

--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key2"
Content-Type: text/plain

value2
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="key1"
Content-Type: text/plain

value1
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file1"; filename="file1.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of C:\path\to\file1.png ...]
--__-----------------------9446182961397930864818
Content-Disposition: form-data; name="file2"; filename="file2.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[... contents of C:\path\to\file2.png ...]
--__-----------------------9446182961397930864818--