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

Implement Http cache #15

Merged
merged 2 commits into from
Aug 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/ResponseCachingSample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void Configure(IApplicationBuilder app)
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
context.Response.Headers["Vary"] = new string[] { "Accept-Encoding", "Non-Existent" };
context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding", "Non-Existent" };

await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
});
Expand Down
5 changes: 3 additions & 2 deletions src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNetCore.ResponseCaching
{
public interface IResponseCache
{
object Get(string key);
// TODO: Set expiry policy in the underlying cache?
void Set(string key, object entry);
void Set(string key, object entry, TimeSpan validFor);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need a context object here. What other things will we need to pass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we need in the context object? Should we add that when we actually need it?

void Remove(string key);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class CachedResponse
{
internal DateTimeOffset Created { get; set; }

internal int StatusCode { get; set; }

internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static object Read(BinaryReader reader)
}

// Serialization Format
// Creation time - UtcTicks (long)
// Status code (int)
// Header count (int)
// Header(s)
Expand All @@ -78,6 +79,7 @@ public static object Read(BinaryReader reader)
// Body (byte[])
private static CachedResponse ReadCachedResponse(BinaryReader reader)
{
var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var statusCode = reader.ReadInt32();
var headerCount = reader.ReadInt32();
var headers = new HeaderDictionary();
Expand All @@ -90,7 +92,7 @@ private static CachedResponse ReadCachedResponse(BinaryReader reader)
var bodyLength = reader.ReadInt32();
var body = reader.ReadBytes(bodyLength);

return new CachedResponse { StatusCode = statusCode, Headers = headers, Body = body };
return new CachedResponse { Created = created, StatusCode = statusCode, Headers = headers, Body = body };
}

// Serialization Format
Expand Down Expand Up @@ -135,6 +137,7 @@ public static void Write(BinaryWriter writer, object entry)
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
{
writer.Write(nameof(CachedResponse));
writer.Write(entry.Created.UtcTicks);
writer.Write(entry.StatusCode);
writer.Write(entry.Headers.Count);
foreach (var header in entry.Headers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@ public void Remove(string key)
}
}

public void Set(string key, object entry)
public void Set(string key, object entry, TimeSpan validFor)
{
try
{
_cache.Set(key, DefaultResponseCacheSerializer.Serialize(entry));
_cache.Set(
key,
DefaultResponseCacheSerializer.Serialize(entry),
new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = validFor
});
}
catch
{
Expand Down
18 changes: 18 additions & 0 deletions src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
/// <summary>
/// Abstracts the system clock to facilitate testing.
/// </summary>
internal interface ISystemClock
{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
DateTimeOffset UtcNow { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ public void Remove(string key)
_cache.Remove(key);
}

public void Set(string key, object entry)
public void Set(string key, object entry, TimeSpan validFor)
{
_cache.Set(key, entry);
_cache.Set(
key,
entry,
new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = validFor
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class ResponseCacheStream : Stream
{
private readonly Stream _innerStream;

public ResponseCacheStream(Stream innerStream)
{
_innerStream = innerStream;
}

public MemoryStream BufferedStream { get; } = new MemoryStream();

public bool BufferingEnabled { get; set; } = true;

public override bool CanRead => _innerStream.CanRead;

public override bool CanSeek => _innerStream.CanSeek;

public override bool CanWrite => _innerStream.CanWrite;

public override long Length => _innerStream.Length;

public override long Position
{
get { return _innerStream.Position; }
set { _innerStream.Position = value; }
}

public void DisableBuffering()
{
BufferingEnabled = false;
BufferedStream.Dispose();
}

public override void SetLength(long value)
{
DisableBuffering();
_innerStream.SetLength(value);
}

public override long Seek(long offset, SeekOrigin origin)
{
DisableBuffering();
return _innerStream.Seek(offset, origin);
}

public override void Flush()
=> _innerStream.Flush();

public override Task FlushAsync(CancellationToken cancellationToken)
=> _innerStream.FlushAsync();

// Underlying stream is write-only, no need to override other read related methods
public override int Read(byte[] buffer, int offset, int count)
=> _innerStream.Read(buffer, offset, count);

public override void Write(byte[] buffer, int offset, int count)
{
try
{
_innerStream.Write(buffer, offset, count);
}
catch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These try/catch's only need to be around the _innerStream operations.

{
DisableBuffering();
throw;
}

if (BufferingEnabled)
{
BufferedStream.Write(buffer, offset, count);
}
}

public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must override Begin/End Write as well, #if net451.

{
try
{
await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
}
catch
{
DisableBuffering();
throw;
}

if (BufferingEnabled)
{
await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken);
}
}

public override void WriteByte(byte value)
{
try
{
_innerStream.WriteByte(value);
}
catch
{
DisableBuffering();
throw;
}

if (BufferingEnabled)
{
BufferedStream.WriteByte(value);
}
}

#if NETSTANDARD1_3
public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
#else
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
#endif
{
return ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
}
#if NETSTANDARD1_3
public void EndWrite(IAsyncResult asyncResult)
#else
public override void EndWrite(IAsyncResult asyncResult)
#endif
{
if (asyncResult == null)
{
throw new ArgumentNullException(nameof(asyncResult));
}
((Task)asyncResult).GetAwaiter().GetResult();
}

private static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state)
{
var tcs = new TaskCompletionSource<int>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted)
{
tcs.TrySetException(t.Exception.InnerExceptions);
}
else if (t.IsCanceled)
{
tcs.TrySetCanceled();
}
else
{
tcs.TrySetResult(0);
}

callback?.Invoke(tcs.Task);
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
return tcs.Task;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class SendFileFeatureWrapper : IHttpSendFileFeature
{
private readonly IHttpSendFileFeature _originalSendFileFeature;
private readonly ResponseCacheStream _responseCacheStream;

public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCacheStream responseCacheStream)
{
_originalSendFileFeature = originalSendFileFeature;
_responseCacheStream = responseCacheStream;
}

// Flush and disable the buffer if anyone tries to call the SendFile feature.
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
_responseCacheStream.DisableBuffering();
return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation);
}
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
/// <summary>
/// Provides access to the normal system clock.
/// </summary>
internal class SystemClock : ISystemClock
{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
public DateTimeOffset UtcNow
{
get
{
return DateTimeOffset.UtcNow;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;

[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-us")]
[assembly: AssemblyCompany("Microsoft Corporation.")]
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
Loading