Skip to content
This repository was archived by the owner on Nov 22, 2018. It is now read-only.

Commit 1d6c5af

Browse files
committed
Add option for VaryBy query string params
1 parent 8c5a5f7 commit 1d6c5af

File tree

9 files changed

+536
-94
lines changed

9 files changed

+536
-94
lines changed

src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
88
internal class CachedVaryBy
99
{
1010
internal StringValues Headers { get; set; }
11+
internal StringValues Params { get; set; }
1112
}
1213
}

src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,26 @@ private static CachedResponse ReadCachedResponse(BinaryReader reader)
9696
}
9797

9898
// Serialization Format
99-
// Headers (comma separated string)
99+
// Headers count
100+
// Headers if count > 0 (comma separated string)
101+
// Params count
102+
// Params if count > 0 (comma separated string)
100103
private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader)
101104
{
102-
var headers = reader.ReadString().Split(',');
105+
var headerCount = reader.ReadInt32();
106+
var headers = new string[headerCount];
107+
for (var index = 0; index < headerCount; index++)
108+
{
109+
headers[index] = reader.ReadString();
110+
}
111+
var paramCount = reader.ReadInt32();
112+
var param = new string[paramCount];
113+
for (var index = 0; index < paramCount; index++)
114+
{
115+
param[index] = reader.ReadString();
116+
}
103117

104-
return new CachedVaryBy { Headers = headers };
118+
return new CachedVaryBy { Headers = headers, Params = param };
105119
}
106120

107121
// See serialization format above
@@ -154,7 +168,18 @@ private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entr
154168
private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry)
155169
{
156170
writer.Write(nameof(CachedVaryBy));
157-
writer.Write(entry.Headers);
171+
172+
writer.Write(entry.Headers.Count);
173+
foreach (var header in entry.Headers)
174+
{
175+
writer.Write(header);
176+
}
177+
178+
writer.Write(entry.Params.Count);
179+
foreach (var param in entry.Params)
180+
{
181+
writer.Write(param);
182+
}
158183
}
159184
}
160185
}

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.IO;
66
using System.Globalization;
7+
using System.Linq;
78
using System.Text;
89
using System.Threading.Tasks;
910
using Microsoft.AspNetCore.Http;
@@ -19,6 +20,8 @@ namespace Microsoft.AspNetCore.ResponseCaching
1920
internal class ResponseCachingContext
2021
{
2122
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
23+
// Use the record separator for delimiting components of the cache key to avoid possible collisions
24+
private static readonly char KeyDelimiter = '\x1e';
2225

2326
private readonly HttpContext _httpContext;
2427
private readonly IResponseCache _cache;
@@ -150,13 +153,19 @@ internal string CreateCacheKey(CachedVaryBy varyBy)
150153

151154
try
152155
{
156+
// Default key
153157
builder
154158
.Append(request.Method.ToUpperInvariant())
155-
.Append(";")
159+
.Append(KeyDelimiter)
156160
.Append(request.Path.Value.ToUpperInvariant());
157161

162+
// Vary by headers
158163
if (varyBy?.Headers.Count > 0)
159164
{
165+
// Append a group separator for the header segment of the cache key
166+
builder.Append(KeyDelimiter)
167+
.Append('H');
168+
160169
// TODO: resolve key format and delimiters
161170
foreach (var header in varyBy.Headers)
162171
{
@@ -169,19 +178,62 @@ internal string CreateCacheKey(CachedVaryBy varyBy)
169178
value = "null";
170179
}
171180

172-
builder.Append(";")
181+
builder.Append(KeyDelimiter)
173182
.Append(header)
174183
.Append("=")
175184
.Append(value);
176185
}
177186
}
178-
// TODO: Parse querystring params
187+
188+
// Vary by query params
189+
if (varyBy?.Params.Count > 0)
190+
{
191+
// Append a group separator for the query parameter segment of the cache key
192+
builder.Append(KeyDelimiter)
193+
.Append('Q');
194+
195+
if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*"))
196+
{
197+
// Vary by all available query params
198+
foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
199+
{
200+
builder.Append(KeyDelimiter)
201+
.Append(query.Key.ToUpperInvariant())
202+
.Append("=")
203+
.Append(query.Value);
204+
}
205+
}
206+
else
207+
{
208+
// TODO: resolve key format and delimiters
209+
foreach (var param in varyBy.Params)
210+
{
211+
// TODO: Normalization of order, case?
212+
var value = _httpContext.Request.Query[param];
213+
214+
// TODO: How to handle null/empty string?
215+
if (StringValues.IsNullOrEmpty(value))
216+
{
217+
value = "null";
218+
}
219+
220+
builder.Append(KeyDelimiter)
221+
.Append(param)
222+
.Append("=")
223+
.Append(value);
224+
}
225+
}
226+
}
179227

180228
// Append custom cache key segment
181229
var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext);
182230
if (!string.IsNullOrEmpty(customKey))
183231
{
184-
builder.Append(";")
232+
// Append a group separator for the custom segment of the cache key
233+
builder.Append(KeyDelimiter)
234+
.Append('C');
235+
236+
builder.Append(KeyDelimiter)
185237
.Append(customKey);
186238
}
187239

@@ -451,20 +503,26 @@ internal void FinalizeCachingHeaders()
451503
// Create the cache entry now
452504
var response = _httpContext.Response;
453505
var varyHeaderValue = response.Headers[HeaderNames.Vary];
506+
var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams;
454507
_cachedResponseValidFor = ResponseCacheControl.SharedMaxAge
455508
?? ResponseCacheControl.MaxAge
456509
?? (ResponseHeaders.Expires - _responseTime)
457510
// TODO: Heuristics for expiration?
458511
?? TimeSpan.FromSeconds(10);
459512

460513
// Check if any VaryBy rules exist
461-
if (!StringValues.IsNullOrEmpty(varyHeaderValue))
514+
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
462515
{
516+
if (varyParamsValue.Count > 1)
517+
{
518+
Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase);
519+
}
520+
463521
var cachedVaryBy = new CachedVaryBy
464522
{
465-
// Only vary by headers for now
466523
// TODO: VaryBy Encoding
467-
Headers = varyHeaderValue
524+
Headers = varyHeaderValue,
525+
Params = varyParamsValue
468526
};
469527

470528
// TODO: Overwrite?
@@ -536,6 +594,9 @@ internal void ShimResponseStream()
536594
{
537595
_httpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
538596
}
597+
598+
// TODO: Move this temporary interface with endpoint to HttpAbstractions
599+
_httpContext.AddResponseCachingFeature();
539600
}
540601

541602
internal void UnshimResponseStream()
@@ -545,6 +606,9 @@ internal void UnshimResponseStream()
545606

546607
// Unshim IHttpSendFileFeature
547608
_httpContext.Features.Set(OriginalSendFileFeature);
609+
610+
// TODO: Move this temporary interface with endpoint to HttpAbstractions
611+
_httpContext.RemoveResponseCachingFeature();
548612
}
549613

550614
private enum ResponseType

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using Microsoft.AspNetCore.ResponseCaching;
6-
using Microsoft.Extensions.Options;
76

87
namespace Microsoft.AspNetCore.Builder
98
{
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Primitives;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching
7+
{
8+
// TODO: Temporary interface for endpoints to specify options for response caching
9+
public class ResponseCachingFeature
10+
{
11+
public StringValues VaryByParams { get; set; }
12+
}
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching
7+
{
8+
// TODO: Temporary interface for endpoints to specify options for response caching
9+
public static class ResponseCachingHttpContextExtensions
10+
{
11+
public static void AddResponseCachingFeature(this HttpContext httpContext)
12+
{
13+
httpContext.Features.Set(new ResponseCachingFeature());
14+
}
15+
16+
public static void RemoveResponseCachingFeature(this HttpContext httpContext)
17+
{
18+
httpContext.Features.Set<ResponseCachingFeature>(null);
19+
}
20+
21+
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
22+
{
23+
return httpContext.Features.Get<ResponseCachingFeature>() ?? new ResponseCachingFeature();
24+
}
25+
}
26+
}

test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
1313
public class DefaultResponseCacheEntrySerializerTests
1414
{
1515
[Fact]
16-
public void SerializeNullObjectThrows()
16+
public void Serialize_NullObject_Throws()
1717
{
1818
Assert.Throws<ArgumentNullException>(() => DefaultResponseCacheSerializer.Serialize(null));
1919
}
2020

2121
[Fact]
22-
public void SerializeUnknownObjectThrows()
22+
public void Serialize_UnknownObject_Throws()
2323
{
2424
Assert.Throws<NotSupportedException>(() => DefaultResponseCacheSerializer.Serialize(new object()));
2525
}
2626

2727
[Fact]
28-
public void RoundTripCachedResponsesSucceeds()
28+
public void RoundTrip_CachedResponses_Succeeds()
2929
{
3030
var headers = new HeaderDictionary();
3131
headers["keyA"] = "valueA";
@@ -42,7 +42,15 @@ public void RoundTripCachedResponsesSucceeds()
4242
}
4343

4444
[Fact]
45-
public void RoundTripCachedVaryBySucceeds()
45+
public void RoundTrip_Empty_CachedVaryBy_Succeeds()
46+
{
47+
var cachedVaryBy = new CachedVaryBy();
48+
49+
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
50+
}
51+
52+
[Fact]
53+
public void RoundTrip_HeadersOnly_CachedVaryBy_Succeeds()
4654
{
4755
var headers = new[] { "headerA", "headerB" };
4856
var cachedVaryBy = new CachedVaryBy()
@@ -53,9 +61,34 @@ public void RoundTripCachedVaryBySucceeds()
5361
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
5462
}
5563

64+
[Fact]
65+
public void RoundTrip_ParamsOnly_CachedVaryBy_Succeeds()
66+
{
67+
var param = new[] { "paramA", "paramB" };
68+
var cachedVaryBy = new CachedVaryBy()
69+
{
70+
Params = param
71+
};
72+
73+
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
74+
}
75+
76+
[Fact]
77+
public void RoundTrip_HeadersAndParams_CachedVaryBy_Succeeds()
78+
{
79+
var headers = new[] { "headerA", "headerB" };
80+
var param = new[] { "paramA", "paramB" };
81+
var cachedVaryBy = new CachedVaryBy()
82+
{
83+
Headers = headers,
84+
Params = param
85+
};
86+
87+
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
88+
}
5689

5790
[Fact]
58-
public void DeserializeInvalidEntriesReturnsNull()
91+
public void Deserialize_InvalidEntries_ReturnsNull()
5992
{
6093
var headers = new[] { "headerA", "headerB" };
6194
var cachedVaryBy = new CachedVaryBy()
@@ -87,6 +120,7 @@ private static void AssertCachedVarybyEqual(CachedVaryBy expected, CachedVaryBy
87120
Assert.NotNull(actual);
88121
Assert.NotNull(expected);
89122
Assert.Equal(expected.Headers, actual.Headers);
123+
Assert.Equal(expected.Params, actual.Params);
90124
}
91125
}
92126
}

0 commit comments

Comments
 (0)