5

How to handle null query parameters in AspNet Core?

Suppose we have a query ?key1=foo1&key1=foo2&key2=&key3=null

When parsing it, I would expect to have some kind of Dictionary> as a result when parsing this URL like:

  • key1 : ["foo1", "foo2"] this should be multiple values under the same key
  • key2 : [""] this should be an empty string
  • key3 : ["null"] this should be a string, as far as I know, null in a URL is just a literal

My question is: how should I handle null query parameters?

Note: I could simply not define the query parameter and assume that inexisting query parameters are null. But I think null should be treated as a valid value in explicit query parameters if required.

According to this thread: How to send NULL in HTTP query string? the standard is to pass the encoded null value: see https://www.w3schools.com/tags/ref_urlencode.asp

so if I want to pass a null value I should do something like: ?key1=foo1&key1=foo2&key2=&key3=%00

The problem is that I don't know how to decode this so that %00 is parsed as a null value.

I have tried the following:

public Dictionary<string, List<string>> CreateFromQuery(string query)
{
    if (query == null)
    {
        return new Dictionary<string, List<string>>();
    }

    var queryDictionary = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(query);

    var result = queryDictionary.ToDictionary(kv => kv.Key, kv => kv.Value.ToList());
    return result;
}

But the %00 is transformed into a "\0" string, not to a null.

Doing a var decodedQuery= HttpUtility.UrlDecode(query); before does not seem to make any difference either.

UPDATE1: After Kacper and Chris Pratt's comments (thank you guys) I went with the Kacper second suggestion for now because I think that it's interesting to have scenarios where the requester wants to differentiate between null query parameters, empty query parameters and inexistent query parameters.

So this is my current implementation:

public class QueryParserFactory
    : IQueryParseable
{
    public Dictionary<string, List<string>> CreateFromQuery(string query)
    {
        if (query == null)
        {
            return new Dictionary<string, List<string>>();
        }

        var queryDecoded = HttpUtility.UrlDecode(query);

        var queryDictionary = QueryHelpers.ParseQuery(queryDecoded);

        var result = queryDictionary
            .ToDictionary(
                kv => kv.Key,
                kv => kv.Value.Select(s => s == "\0" ? null : s).ToList());
        return result;
    }
}

And if someone is interested, below there are all the unit tests I can think of:

public static class CreateFromQueryTests
{
    public class Given_An_Empty_Query_String_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;

        protected override void Given()
        {
            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Not_Have_Any_Key()
        {
            _result.Keys.Count.Should().Be(0);
        }

        [Fact]
        public void Then_It_Should_Not_Have_Any_Items_In_Dictionary()
        {
            _result.Count.Should().Be(0);
        }
    }

    public class Given_A_Query_String_With_Empty_Values_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;
        private List<string> _expectedValueForKey1;
        private List<string> _expectedValueForKey2;

        protected override void Given()
        {
            _expectedValueForKey1 = new List<string>
            {
                string.Empty
            };

            _expectedValueForKey2 = new List<string>
            {
                string.Empty
            };

            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("?key1=&key2=");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Have_Key_For_All_Fulfilled_Parameters()
        {
            _result.Keys.Count.Should().Be(2);
        }

        [Fact]
        public void Then_It_Should_Have_Empty_Value_For_The_First_Key_Parameter()
        {
            _result["key1"].Should().BeEquivalentTo(_expectedValueForKey1);
        }

        [Fact]
        public void Then_It_Should_Have_Empty_Value_For_The_Second_Key_Parameter()
        {
            _result["key2"].Should().BeEquivalentTo(_expectedValueForKey2);
        }
    }

    public class Given_A_Query_String_With_Single_Values_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;
        private List<string> _expectedValueForKey1;
        private List<string> _expectedValueForKey2;

        protected override void Given()
        {
            _expectedValueForKey1 = new List<string>()
            {
                "value1"
            };

            _expectedValueForKey2 = new List<string>()
            {
                "value2"
            };

            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("?key1=value1&key2=value2");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Have_Key_For_All_Fulfilled_Parameters()
        {
            _result.Keys.Count.Should().Be(2);
        }

        [Fact]
        public void Then_It_Should_Have_The_Correct_Multiple_Values_For_Keys_With_Multiple_Parameters()
        {
            _result["key1"].Should().BeEquivalentTo(_expectedValueForKey1);
        }

        [Fact]
        public void Then_It_Should_Have_The_Correct_Single_Value_For_Keys_With_One_Parameter()
        {
            _result["key2"].Should().BeEquivalentTo(_expectedValueForKey2);
        }

        [Fact]
        public void Then_It_Should_Not_Have_Entries_For_Inexistent_Parameters()
        {
            _result.TryGetValue("key3", out List<string> _).Should().BeFalse();
        }
    }

    public class Given_A_Query_String_With_Multiple_Values_For_The_Same_Key_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;
        private List<string> _expectedValueForKey1;

        protected override void Given()
        {
            _expectedValueForKey1 = new List<string>()
            {
                "value1",
                "value2",
                "value3"
            };

            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("?key1=value1&key1=value2&key1=value3");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Have_Only_One_Key()
        {
            _result.Keys.Count.Should().Be(1);
        }

        [Fact]
        public void Then_It_Should_Have_The_Correct_Multiple_Values_For_Keys_With_Multiple_Parameters()
        {
            _result["key1"].Should().BeEquivalentTo(_expectedValueForKey1);
        }

        [Fact]
        public void Then_It_Should_Not_Have_Entries_For_Inexistent_Parameters()
        {
            _result.TryGetValue("key2", out List<string> _).Should().BeFalse();
        }
    }

    public class Given_A_Query_String_With_Non_Url_Encoded_Null_Values_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;
        private List<string> _expectedValueForKey1;
        private List<string> _expectedValueForKey2;

        protected override void Given()
        {
            _expectedValueForKey1 = new List<string>()
            {
                "null"
            };

            _expectedValueForKey2 = new List<string>()
            {
                "null"
            };

            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("?key1=null&key2=null");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Have_Key_For_All_Fulfilled_Parameters()
        {
            _result.Keys.Count.Should().Be(2);
        }

        [Fact]
        public void Then_It_Should_Have_A_Null_Literal_For_The_First_Parameter()
        {
            _result["key1"].Should().BeEquivalentTo(_expectedValueForKey1);
        }

        [Fact]
        public void Then_It_Should_Have_A_Null_Literal_For_The_Second_Parameter()
        {
            _result["key2"].Should().BeEquivalentTo(_expectedValueForKey2);
        }

        [Fact]
        public void Then_It_Should_Not_Have_Entries_For_Inexistent_Parameters()
        {
            _result.TryGetValue("key3", out List<string> _).Should().BeFalse();
        }
    }

    public class Given_A_Query_String_With_Url_Encoded_Null_Values_When_Creating_From_A_Query
        : Given_When_Then_Test
    {
        private QueryParserFactory _sut;
        private Dictionary<string, List<string>> _result;
        private List<string> _expectedValueForKey1;
        private List<string> _expectedValueForKey2;

        protected override void Given()
        {
            _expectedValueForKey1 = new List<string>()
            {
                null
            };

            _expectedValueForKey2 = new List<string>()
            {
                null
            };

            _sut = new QueryParserFactory();
        }

        protected override void When()
        {
            _result = _sut.CreateFromQuery("?key1=%00&key2=%00");
        }

        [Fact]
        public void Then_It_Should_Return_A_Valid_Result()
        {
            _result.Should().NotBeNull();
        }

        [Fact]
        public void Then_It_Should_Have_Key_For_All_Fulfilled_Parameters()
        {
            _result.Keys.Count.Should().Be(2);
        }

        [Fact]
        public void Then_It_Should_Have_A_Null_Literal_For_The_First_Parameter()
        {
            _result["key1"].Should().BeEquivalentTo(_expectedValueForKey1);
        }

        [Fact]
        public void Then_It_Should_Have_A_Null_Literal_For_The_Second_Parameter()
        {
            _result["key2"].Should().BeEquivalentTo(_expectedValueForKey2);
        }

        [Fact]
        public void Then_It_Should_Not_Have_Entries_For_Inexistent_Parameters()
        {
            _result.TryGetValue("key3", out List<string> _).Should().BeFalse();
        }
    }
}
diegosasw
  • 13,734
  • 16
  • 95
  • 159

2 Answers2

3

The way to pass a null is to not pass a value at all, or exclude the key entirely, i.e.:

?key1=foo1&key1=foo2&key2=&key3=

Or simply:

?key1=foo1&key1=foo2&key2=

For your key2 param, you should be aware that there's no way to pass an empty string. ASP.NET Core will interpret that as a null value. If you have a string property that should not actually be null (i.e. you want it to always be an empty string in such cases), then you can handle that via a custom getter.

private string key2;
public string Key2
{
    get => key2 ?? string.Empty;
    set => key2 = value;
}

Then, in cases where it's set to a null value, it will materialize as an empty string instead.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    Thanks. The problem is that if I don't pass a value like `?key2=` how do you differentiate between an empty string and a null value? Empty string could be a valid query parameter different than null. Also if I exclude the query parameter altogether and there is no key3, how do you differentiate whether the client wants, for example, to filter by key3=null if null is a valid value? That's why I wanted to have something explicit for 3 scenarios where the client specifies that a parameter is null, or a parameter is empty or there is no parameter at all. – diegosasw Jul 23 '18 at 13:02
  • It's not possible. There's not enough context to tell whether the user actually *intended* an empty string value or left it blank (which is effectively the same thing). As a result, every framework individually makes a determination about how they're going to handle empty request values. In the case of ASP.NET Core, it's interpreted as null. However, regardless, you cannot have both. For example, you could always create a custom modelbinder to force it to be interpreted as an empty string instead, but then you couldn't get null values any more. It's either/or, not both. – Chris Pratt Jul 23 '18 at 13:07
2

I can't find any built in thing to make it. So I have two options depending what will fit for you.

1.

public Dictionary<string, List<string>> CreateFromQuery(string query)
{
    if (query == null)
    {
        return new Dictionary<string, List<string>>();
    }

    var queryDictionary = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(query);

    var result = queryDictionary
       .ToDictionary(
           kv => kv.Key, 
           kv => kv.Value.Select(s => s.Trim("\0")).ToList()); //There you will have String.Empty
    return result;
}

2.

public Dictionary<string, List<string>> CreateFromQuery(string query)
{
    if (query == null)
    {
        return new Dictionary<string, List<string>>();
    }

    var queryDictionary = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(query);

    var result = queryDictionary
       .ToDictionary(
           kv => kv.Key, 
           kv => kv.Value.Select(s => s == "\0" ? null : s).ToList()); //There you will have nulls
    return result;
}
Kacper
  • 451
  • 6
  • 17
  • thank you. Do you know if assuming that `key3=%00` should be transformed into `null` and `key3=null` should be transformed into `"null"` string literal is the right assumption? – diegosasw Jul 23 '18 at 12:43
  • Sorry, could yoy clarify? to mean converting from HTTP Query to c# or from cz3 to Http? – Kacper Jul 23 '18 at 12:48
  • I mean.. is it correct to assume that a null query parameter in a http get url should be represented with %00 or is it better to represent it with null keyword in the url? – diegosasw Jul 23 '18 at 16:14