Skip to content

Commit c3c55f6

Browse files
author
Bart Koelman
committed
Workaround for bug dotnet/roslyn#13624
1 parent f036db7 commit c3c55f6

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Http.Features;
5+
using Microsoft.Extensions.Primitives;
6+
7+
namespace JsonApiDotNetCore.Middleware
8+
{
9+
/// <summary>
10+
/// Replacement implementation for the ASP.NET built-in <see cref="QueryFeature" />, to workaround bug https://github.com/dotnet/aspnetcore/issues/33394.
11+
/// This is identical to the built-in version, except it calls <see cref="FixedQueryHelpers.ParseNullableQuery" />.
12+
/// </summary>
13+
internal sealed class FixedQueryFeature : IQueryFeature
14+
{
15+
// Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624
16+
private static readonly Func<IFeatureCollection, IHttpRequestFeature> NullRequestFeature = _ => null;
17+
18+
private FeatureReferences<IHttpRequestFeature> _features;
19+
20+
private string _original;
21+
private IQueryCollection _parsedValues;
22+
23+
private IHttpRequestFeature HttpRequestFeature => _features.Fetch(ref _features.Cache, NullRequestFeature);
24+
25+
/// <inheritdoc />
26+
public IQueryCollection Query
27+
{
28+
get
29+
{
30+
if (_features.Collection == null)
31+
{
32+
if (_parsedValues == null)
33+
{
34+
_parsedValues = QueryCollection.Empty;
35+
}
36+
37+
return _parsedValues;
38+
}
39+
40+
string current = HttpRequestFeature.QueryString;
41+
42+
if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal))
43+
{
44+
_original = current;
45+
46+
Dictionary<string, StringValues> result = FixedQueryHelpers.ParseNullableQuery(current);
47+
48+
if (result == null)
49+
{
50+
_parsedValues = QueryCollection.Empty;
51+
}
52+
else
53+
{
54+
_parsedValues = new QueryCollection(result);
55+
}
56+
}
57+
58+
return _parsedValues;
59+
}
60+
set
61+
{
62+
_parsedValues = value;
63+
64+
if (_features.Collection != null)
65+
{
66+
if (value == null)
67+
{
68+
_original = string.Empty;
69+
HttpRequestFeature.QueryString = string.Empty;
70+
}
71+
else
72+
{
73+
_original = QueryString.Create(_parsedValues).ToString();
74+
HttpRequestFeature.QueryString = _original;
75+
}
76+
}
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Initializes a new instance of <see cref="QueryFeature" />.
82+
/// </summary>
83+
/// <param name="features">
84+
/// The <see cref="IFeatureCollection" /> to initialize.
85+
/// </param>
86+
public FixedQueryFeature(IFeatureCollection features)
87+
{
88+
ArgumentGuard.NotNull(features, nameof(features));
89+
90+
_features.Initalize(features);
91+
}
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.AspNetCore.WebUtilities;
4+
using Microsoft.Extensions.Primitives;
5+
6+
#pragma warning disable AV1008 // Class should not be static
7+
#pragma warning disable AV1708 // Type name contains term that should be avoided
8+
#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type
9+
#pragma warning disable AV1532 // Loop statement contains nested loop
10+
11+
namespace JsonApiDotNetCore.Middleware
12+
{
13+
/// <summary>
14+
/// Replacement implementation for the ASP.NET built-in <see cref="QueryHelpers" />, to workaround bug https://github.com/dotnet/aspnetcore/issues/33394.
15+
/// This is identical to the built-in version, except it properly un-escapes query string keys without a value.
16+
/// </summary>
17+
internal static class FixedQueryHelpers
18+
{
19+
/// <summary>
20+
/// Parse a query string into its component key and value parts.
21+
/// </summary>
22+
/// <param name="queryString">
23+
/// The raw query string value, with or without the leading '?'.
24+
/// </param>
25+
/// <returns>
26+
/// A collection of parsed keys and values, null if there are no entries.
27+
/// </returns>
28+
public static Dictionary<string, StringValues> ParseNullableQuery(string queryString)
29+
{
30+
var accumulator = new KeyValueAccumulator();
31+
32+
if (string.IsNullOrEmpty(queryString) || queryString == "?")
33+
{
34+
return null;
35+
}
36+
37+
int scanIndex = 0;
38+
39+
if (queryString[0] == '?')
40+
{
41+
scanIndex = 1;
42+
}
43+
44+
int textLength = queryString.Length;
45+
int equalIndex = queryString.IndexOf('=');
46+
47+
if (equalIndex == -1)
48+
{
49+
equalIndex = textLength;
50+
}
51+
52+
while (scanIndex < textLength)
53+
{
54+
int delimiterIndex = queryString.IndexOf('&', scanIndex);
55+
56+
if (delimiterIndex == -1)
57+
{
58+
delimiterIndex = textLength;
59+
}
60+
61+
if (equalIndex < delimiterIndex)
62+
{
63+
while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex]))
64+
{
65+
++scanIndex;
66+
}
67+
68+
string name = queryString.Substring(scanIndex, equalIndex - scanIndex);
69+
string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1);
70+
accumulator.Append(Uri.UnescapeDataString(name.Replace('+', ' ')), Uri.UnescapeDataString(value.Replace('+', ' ')));
71+
equalIndex = queryString.IndexOf('=', delimiterIndex);
72+
73+
if (equalIndex == -1)
74+
{
75+
equalIndex = textLength;
76+
}
77+
}
78+
else
79+
{
80+
if (delimiterIndex > scanIndex)
81+
{
82+
// original code:
83+
// accumulator.Append(queryString.Substring(scanIndex, delimiterIndex - scanIndex), string.Empty);
84+
85+
// replacement:
86+
string name = queryString.Substring(scanIndex, delimiterIndex - scanIndex);
87+
accumulator.Append(Uri.UnescapeDataString(name.Replace('+', ' ')), string.Empty);
88+
}
89+
}
90+
91+
scanIndex = delimiterIndex + 1;
92+
}
93+
94+
if (!accumulator.HasValues)
95+
{
96+
return null;
97+
}
98+
99+
return accumulator.GetResults();
100+
}
101+
}
102+
}

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using JsonApiDotNetCore.Serialization;
1313
using JsonApiDotNetCore.Serialization.Objects;
1414
using Microsoft.AspNetCore.Http;
15+
using Microsoft.AspNetCore.Http.Features;
1516
using Microsoft.AspNetCore.Mvc;
1617
using Microsoft.AspNetCore.Mvc.Controllers;
1718
using Microsoft.AspNetCore.Routing;
@@ -78,6 +79,9 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
7879
httpContext.RegisterJsonApiRequest();
7980
}
8081

82+
// Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394
83+
httpContext.Features.Set<IQueryFeature>(new FixedQueryFeature(httpContext.Features));
84+
8185
await _next(httpContext);
8286
}
8387

0 commit comments

Comments
 (0)