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:
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();
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With