Portable Class Library (PCL) Version Of HttpUtility.ParseQueryString
Asked Answered
G

4

17

Is there a Portable Class Library (PCL) version Of HttpUtility.ParseQueryString contained in System.Web or some code I could use? I want to read a very complex URL.

Gloria answered 28/11, 2013 at 14:24 Comment(4)
What's wrong with referencing System.Web?Saraisaraiya
can you show us your complex URLTybalt
I can't reference System.Web in a Portable Class Library Project.Gloria
This is an example of a simpler URL I am trying to parse journeyplanner.tfl.gov.uk/user/…Gloria
G
24

HttpUtility.ParseQueryString returns HttpValueCollection (Internal Class) which inherits from NameValueCollection. NameValueCollection is a collection of key value pairs like a dictionary but it supports duplicates, maintains order and only implements IEnumerable (This collection is pre-generics). NameValueCollection is not supported in PCL.

My solution (Partly lifted and modified from the .NET framework) is to substitute HttpValueCollection with Collection<HttpValue> where HttpValue is just a key value pair.

public sealed class HttpUtility
{
    public static HttpValueCollection ParseQueryString(string query)
    {
        if (query == null)
        {
            throw new ArgumentNullException("query");
        }

        if ((query.Length > 0) && (query[0] == '?'))
        {
            query = query.Substring(1);
        }

        return new HttpValueCollection(query, true);
    }
}

public sealed class HttpValue
{
    public HttpValue()
    {
    }

    public HttpValue(string key, string value)
    {
        this.Key = key;
        this.Value = value;
    }

    public string Key { get; set; }
    public string Value { get; set; }
}

public class HttpValueCollection : Collection<HttpValue>
{
    #region Constructors

    public HttpValueCollection()
    {
    }

    public HttpValueCollection(string query)
        : this(query, true)
    {
    }

    public HttpValueCollection(string query, bool urlencoded)
    {
        if (!string.IsNullOrEmpty(query))
        {
            this.FillFromString(query, urlencoded);
        }
    } 

    #endregion

    #region Parameters

    public string this[string key]
    {
        get { return this.First(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Value; }
        set { this.First(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Value = value; }
    }

    #endregion

    #region Public Methods

    public void Add(string key, string value)
    {
        this.Add(new HttpValue(key, value));
    }

    public bool ContainsKey(string key)
    {
        return this.Any(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase));
    }

    public string[] GetValues(string key)
    {
        return this.Where(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray();
    }

    public void Remove(string key)
    {
        this.Where(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase))
            .ToList()
            .ForEach(x => this.Remove(x));
    }

    public override string ToString()
    {
        return this.ToString(true);
    }

    public virtual string ToString(bool urlencoded)
    {
        return this.ToString(urlencoded, null);
    }

    public virtual string ToString(bool urlencoded, IDictionary excludeKeys)
    {
        if (this.Count == 0)
        {
            return string.Empty;
        }

        StringBuilder stringBuilder = new StringBuilder();

        foreach (HttpValue item in this)
        {
            string key = item.Key;

            if ((excludeKeys == null) || !excludeKeys.Contains(key))
            {
                string value = item.Value;

                if (urlencoded)
                {
                    // If .NET 4.5 and above (Thanks @Paya)
                    key = WebUtility.UrlDecode(key);
                    // If .NET 4.0 use this instead.
                    // key = Uri.EscapeDataString(key);
                }

                if (stringBuilder.Length > 0)
                {
                    stringBuilder.Append('&');
                }

                stringBuilder.Append((key != null) ? (key + "=") : string.Empty);

                if ((value != null) && (value.Length > 0))
                {
                    if (urlencoded)
                    {
                        value = Uri.EscapeDataString(value);
                    }

                    stringBuilder.Append(value);
                }
            }
        }

        return stringBuilder.ToString();
    } 

    #endregion

    #region Private Methods

    private void FillFromString(string query, bool urlencoded)
    {
        int num = (query != null) ? query.Length : 0;
        for (int i = 0; i < num; i++)
        {
            int startIndex = i;
            int num4 = -1;
            while (i < num)
            {
                char ch = query[i];
                if (ch == '=')
                {
                    if (num4 < 0)
                    {
                        num4 = i;
                    }
                }
                else if (ch == '&')
                {
                    break;
                }
                i++;
            }
            string str = null;
            string str2 = null;
            if (num4 >= 0)
            {
                str = query.Substring(startIndex, num4 - startIndex);
                str2 = query.Substring(num4 + 1, (i - num4) - 1);
            }
            else
            {
                str2 = query.Substring(startIndex, i - startIndex);
            }

            if (urlencoded)
            {
                this.Add(Uri.UnescapeDataString(str), Uri.UnescapeDataString(str2));
            }
            else
            {
                this.Add(str, str2);
            }

            if ((i == (num - 1)) && (query[i] == '&'))
            {
                this.Add(null, string.Empty);
            }
        }
    } 

    #endregion
}

UPDATE

Updated so that HttpValueCollection now inherits from Collection rather than List as highlighted in the comments.

UPDATE 2

Updated to use WebUtility.UrlDecode if using .NET 4.5, thanks to @Paya.

Gloria answered 29/11, 2013 at 11:24 Comment(4)
warning: inheriting from List<T>Supermundane
This almost works for me, but I am missing .ForEach in Remove(string key)Scandalmonger
var list = this.Where(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach(var x in list) { this.Remove(x); }Scandalmonger
WebUtility.UrlDecode seems like a better choice if you are on .NET 4.5Hotchkiss
A
5

You can also implement it like this:

public static class HttpUtility
{
    public static Dictionary<string, string> ParseQueryString(Uri uri)
    {
        var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1); // +1 for skipping '?'
        var pairs = query.Split('&');
        return pairs
            .Select(o => o.Split('='))
            .Where(items => items.Count() == 2)
            .ToDictionary(pair => Uri.UnescapeDataString(pair[0]),
                pair => Uri.UnescapeDataString(pair[1]));
    }
}

Here is a Unit test for that:

public class HttpParseQueryValuesTests
{
    [TestCase("http://www.example.com", 0, "", "")]
    [TestCase("http://www.example.com?query=value", 1, "query", "value")]
    public void When_parsing_http_query_then_should_have_these_values(string uri, int expectedParamCount,
        string expectedKey, string expectedValue)
    {
        var queryParams = HttpUtility.ParseQueryString(new Uri(uri));
        queryParams.Count.Should().Be(expectedParamCount);

        if (queryParams.Count > 0)
            queryParams[expectedKey].Should().Be(expectedValue);
    }
}
Axum answered 13/9, 2014 at 14:52 Comment(2)
The disadvantage of this approach is that it does not allow arguments with duplicate keys. Also, it does not handle arguments will empty values. However, this will probably fit most peoples needs with more simplicity.Gloria
There is nothing in the URL spec that stops the use of duplicate keys (See this question).Gloria
W
1

My Flurl library is a PCL that parses query strings into IDictionary<string, object> when you instantiate a Url object from a string:

using Flurl;

var url = new Url("http://...");
// get values from url.QueryParams dictionary

The relevant parsing logic is here. Flurl is small, but feel free to swipe just those bits if you want.

Whinchat answered 25/9, 2014 at 16:11 Comment(0)
F
1

I made a nuget package today that does basic query building and parsing. It's made for personal usage but available from the nuget.com repo. For personal usage means it may not be fully compliant with the 'http query specs'. Nuget link here

It's based on a dictionary so doesn't support duplicate keys, mainly because I don't know why you would want that... (can anyone enlighten me?)

It has 1 class representing a query that supports adding, getting parameters, checking if it contains a key... And a static method to parse a key and return a query instance.

Finding answered 4/5, 2016 at 23:1 Comment(2)
1. Using a dictionary will cause duplicate key exceptions if you don't handle duplicate keys, this could cause your site to error. Hackers often try to get sites to error by passing random stuff, including duplicate keys. 2. Using a duplicate key is another way of accepting collections of things. Typically, you'd have to use comma separated values to achieve this.Gloria
I handle the exception a dictionary throws when adding a duplicate key. If this happens I replace the value with the new one, so the hacker part is not a problem here. I never knew that 2 was possible. Is this collection thing with duplicate keys a standardized way of doing that? If it is then I'll look into it.Finding

© 2022 - 2024 — McMap. All rights reserved.