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

Commit 7190412

Browse files
committed
Tigger caching header finalization on write #92
1 parent d43f051 commit 7190412

File tree

8 files changed

+241
-156
lines changed

8 files changed

+241
-156
lines changed

src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
88
{
99
public interface IResponseCache
1010
{
11+
IResponseCacheEntry Get(string key);
1112
Task<IResponseCacheEntry> GetAsync(string key);
13+
14+
void Set(string key, IResponseCacheEntry entry, TimeSpan validFor);
1215
Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor);
1316
}
1417
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.Extensions.Caching.Memory;
7-
using Microsoft.Net.Http.Headers;
7+
using Microsoft.Extensions.Internal;
88

99
namespace Microsoft.AspNetCore.ResponseCaching.Internal
1010
{
@@ -22,34 +22,39 @@ public MemoryResponseCache(IMemoryCache cache)
2222
_cache = cache;
2323
}
2424

25-
public Task<IResponseCacheEntry> GetAsync(string key)
25+
public IResponseCacheEntry Get(string key)
2626
{
2727
var entry = _cache.Get(key);
2828

2929
var memoryCachedResponse = entry as MemoryCachedResponse;
3030
if (memoryCachedResponse != null)
3131
{
32-
return Task.FromResult<IResponseCacheEntry>(new CachedResponse
32+
return new CachedResponse
3333
{
3434
Created = memoryCachedResponse.Created,
3535
StatusCode = memoryCachedResponse.StatusCode,
3636
Headers = memoryCachedResponse.Headers,
3737
Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength)
38-
});
38+
};
3939
}
4040
else
4141
{
42-
return Task.FromResult(entry as IResponseCacheEntry);
42+
return entry as IResponseCacheEntry;
4343
}
4444
}
4545

46-
public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
46+
public Task<IResponseCacheEntry> GetAsync(string key)
47+
{
48+
return Task.FromResult(Get(key));
49+
}
50+
51+
public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
4752
{
4853
var cachedResponse = entry as CachedResponse;
4954
if (cachedResponse != null)
5055
{
5156
var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize);
52-
await cachedResponse.Body.CopyToAsync(segmentStream);
57+
cachedResponse.Body.CopyTo(segmentStream);
5358

5459
_cache.Set(
5560
key,
@@ -77,5 +82,11 @@ public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan valid
7782
});
7883
}
7984
}
85+
86+
public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
87+
{
88+
Set(key, entry, validFor);
89+
return TaskCache.CompletedTask;
90+
}
8091
}
8192
}

src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<PackageReference Include="Microsoft.AspNetCore.Http" Version="1.2.0-*" />
1818
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="1.2.0-*" />
1919
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.2.0-*" />
20-
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="1.2.0-*" PrivateAssets="All"/>
20+
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="1.2.0-*" PrivateAssets="All" />
2121
</ItemGroup>
2222

2323
</Project>

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ public class ResponseCachingMiddleware
2525
private readonly IResponseCachingPolicyProvider _policyProvider;
2626
private readonly IResponseCache _cache;
2727
private readonly IResponseCachingKeyProvider _keyProvider;
28-
private readonly Func<object, Task> _onStartingCallback;
2928

3029
public ResponseCachingMiddleware(
3130
RequestDelegate next,
@@ -66,7 +65,6 @@ public ResponseCachingMiddleware(
6665
_policyProvider = policyProvider;
6766
_cache = cache;
6867
_keyProvider = keyProvider;
69-
_onStartingCallback = state => OnResponseStartingAsync((ResponseCachingContext)state);
7068
}
7169

7270
public async Task Invoke(HttpContext httpContext)
@@ -90,13 +88,10 @@ public async Task Invoke(HttpContext httpContext)
9088

9189
try
9290
{
93-
// Subscribe to OnStarting event
94-
httpContext.Response.OnStarting(_onStartingCallback, context);
95-
9691
await _next(httpContext);
9792

9893
// If there was no response body, check the response headers now. We can cache things like redirects.
99-
await OnResponseStartingAsync(context);
94+
await StartResponseAsync(context);
10095

10196
// Finalize the cache entry
10297
await FinalizeCacheBodyAsync(context);
@@ -219,10 +214,17 @@ internal async Task<bool> TryServeFromCacheAsync(ResponseCachingContext context)
219214
return false;
220215
}
221216

222-
internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
217+
218+
/// <summary>
219+
/// Finalize cache headers.
220+
/// </summary>
221+
/// <param name="context"></param>
222+
/// <returns><c>true</c> if a vary by entry needs to be stored in the cache; otherwise <c>false</c>.</returns>
223+
private bool OnFinalizeCacheHeaders(ResponseCachingContext context)
223224
{
224225
if (_policyProvider.IsResponseCacheable(context))
225226
{
227+
var storeVaryByEntry = false;
226228
context.ShouldCacheResponse = true;
227229

228230
// Create the cache entry now
@@ -262,7 +264,7 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
262264

263265
// Always overwrite the CachedVaryByRules to update the expiry information
264266
_logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
265-
await _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
267+
storeVaryByEntry = true;
266268

267269
context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
268270
}
@@ -290,11 +292,29 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
290292
context.CachedResponse.Headers[header.Key] = header.Value;
291293
}
292294
}
295+
296+
return storeVaryByEntry;
293297
}
294-
else
298+
299+
context.ResponseCachingStream.DisableBuffering();
300+
return false;
301+
}
302+
303+
internal void FinalizeCacheHeaders(ResponseCachingContext context)
304+
{
305+
if (OnFinalizeCacheHeaders(context))
306+
{
307+
_cache.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
308+
}
309+
}
310+
311+
internal Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
312+
{
313+
if (OnFinalizeCacheHeaders(context))
295314
{
296-
context.ResponseCachingStream.DisableBuffering();
315+
return _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
297316
}
317+
return TaskCache.CompletedTask;
298318
}
299319

300320
internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
@@ -327,19 +347,38 @@ internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
327347
}
328348
}
329349

330-
internal Task OnResponseStartingAsync(ResponseCachingContext context)
350+
/// <summary>
351+
/// Mark the response as started and set the response time if no reponse was started yet.
352+
/// </summary>
353+
/// <param name="context"></param>
354+
/// <returns><c>true</c> if the response was not started before this call; otherwise <c>false</c>.</returns>
355+
private bool OnStartResponse(ResponseCachingContext context)
331356
{
332357
if (!context.ResponseStarted)
333358
{
334359
context.ResponseStarted = true;
335360
context.ResponseTime = _options.SystemClock.UtcNow;
336361

337-
return FinalizeCacheHeadersAsync(context);
362+
return true;
338363
}
339-
else
364+
return false;
365+
}
366+
367+
internal void StartResponse(ResponseCachingContext context)
368+
{
369+
if (OnStartResponse(context))
340370
{
341-
return TaskCache.CompletedTask;
371+
FinalizeCacheHeaders(context);
372+
}
373+
}
374+
375+
internal Task StartResponseAsync(ResponseCachingContext context)
376+
{
377+
if (OnStartResponse(context))
378+
{
379+
return FinalizeCacheHeadersAsync(context);
342380
}
381+
return TaskCache.CompletedTask;
343382
}
344383

345384
internal static void AddResponseCachingFeature(HttpContext context)
@@ -355,7 +394,12 @@ internal void ShimResponseStream(ResponseCachingContext context)
355394
{
356395
// Shim response stream
357396
context.OriginalResponseStream = context.HttpContext.Response.Body;
358-
context.ResponseCachingStream = new ResponseCachingStream(context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize);
397+
context.ResponseCachingStream = new ResponseCachingStream(
398+
context.OriginalResponseStream,
399+
_options.MaximumBodySize,
400+
StreamUtilities.BodySegmentSize,
401+
() => StartResponse(context),
402+
() => StartResponseAsync(context));
359403
context.HttpContext.Response.Body = context.ResponseCachingStream;
360404

361405
// Shim IHttpSendFileFeature

src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ internal class ResponseCachingStream : Stream
1414
private readonly long _maxBufferSize;
1515
private readonly int _segmentSize;
1616
private SegmentWriteStream _segmentWriteStream;
17+
private Action _startResponseCallback;
18+
private Func<Task> _startResponseCallbackAsync;
1719

18-
internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize)
20+
internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback, Func<Task> startResponseCallbackAsync)
1921
{
2022
_innerStream = innerStream;
2123
_maxBufferSize = maxBufferSize;
2224
_segmentSize = segmentSize;
25+
_startResponseCallback = startResponseCallback;
26+
_startResponseCallbackAsync = startResponseCallbackAsync;
2327
_segmentWriteStream = new SegmentWriteStream(_segmentSize);
2428
}
2529

@@ -71,10 +75,32 @@ public override long Seek(long offset, SeekOrigin origin)
7175
}
7276

7377
public override void Flush()
74-
=> _innerStream.Flush();
78+
{
79+
try
80+
{
81+
_startResponseCallback();
82+
_innerStream.Flush();
83+
}
84+
catch
85+
{
86+
DisableBuffering();
87+
throw;
88+
}
89+
}
7590

76-
public override Task FlushAsync(CancellationToken cancellationToken)
77-
=> _innerStream.FlushAsync();
91+
public override async Task FlushAsync(CancellationToken cancellationToken)
92+
{
93+
try
94+
{
95+
await _startResponseCallbackAsync();
96+
await _innerStream.FlushAsync();
97+
}
98+
catch
99+
{
100+
DisableBuffering();
101+
throw;
102+
}
103+
}
78104

79105
// Underlying stream is write-only, no need to override other read related methods
80106
public override int Read(byte[] buffer, int offset, int count)
@@ -84,6 +110,7 @@ public override void Write(byte[] buffer, int offset, int count)
84110
{
85111
try
86112
{
113+
_startResponseCallback();
87114
_innerStream.Write(buffer, offset, count);
88115
}
89116
catch
@@ -109,6 +136,7 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
109136
{
110137
try
111138
{
139+
await _startResponseCallbackAsync();
112140
await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
113141
}
114142
catch

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True()
359359
}
360360

361361
[Fact]
362-
public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
362+
public async Task StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
363363
{
364364
var clock = new TestClock
365365
{
@@ -372,13 +372,13 @@ public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsRespo
372372
var context = TestUtils.CreateTestContext();
373373
context.ResponseTime = null;
374374

375-
await middleware.OnResponseStartingAsync(context);
375+
await middleware.StartResponseAsync(context);
376376

377377
Assert.Equal(clock.UtcNow, context.ResponseTime);
378378
}
379379

380380
[Fact]
381-
public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce()
381+
public async Task StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce()
382382
{
383383
var clock = new TestClock
384384
{
@@ -392,12 +392,12 @@ public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsRespo
392392
var initialTime = clock.UtcNow;
393393
context.ResponseTime = null;
394394

395-
await middleware.OnResponseStartingAsync(context);
395+
await middleware.StartResponseAsync(context);
396396
Assert.Equal(initialTime, context.ResponseTime);
397397

398398
clock.UtcNow += TimeSpan.FromSeconds(10);
399399

400-
await middleware.OnResponseStartingAsync(context);
400+
await middleware.StartResponseAsync(context);
401401
Assert.NotEqual(clock.UtcNow, context.ResponseTime);
402402
Assert.Equal(initialTime, context.ResponseTime);
403403
}
@@ -790,10 +790,9 @@ public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse()
790790
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
791791
var context = TestUtils.CreateTestContext();
792792

793-
context.ShouldCacheResponse = false;
794793
middleware.ShimResponseStream(context);
795794
await context.HttpContext.Response.WriteAsync(new string('0', 10));
796-
795+
context.ShouldCacheResponse = false;
797796

798797
await middleware.FinalizeCacheBodyAsync(context);
799798

0 commit comments

Comments
 (0)