Rough outline of middleware, sample, and test projects.

This commit is contained in:
Chris R 2015-05-22 12:53:34 -07:00 committed by John Luo
parent ed9f238eda
commit a14bb69d6a
16 changed files with 484 additions and 1 deletions

3
.gitignore vendored
View File

@ -23,4 +23,5 @@ nuget.exe
*DS_Store
*.ncrunchsolution
*.*sdf
*.ipch
*.ipch
project.lock.json

50
ResponseCaching.sln Normal file
View File

@ -0,0 +1,50 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22823.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.ResponseCaching", "src\Microsoft.AspNet.ResponseCaching\Microsoft.AspNet.ResponseCaching.xproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C51DF5BD-B53D-4795-BC01-A9AB066BF286}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{89A50974-E9D4-4F87-ACF2-6A6005E64931}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.xproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F787A492-C2FF-4569-A663-F8F24B900657}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.ResponseCaching.Tests", "test\Microsoft.AspNet.ResponseCaching.Tests\Microsoft.AspNet.ResponseCaching.Tests.xproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.Build.0 = Release|Any CPU
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.Build.0 = Release|Any CPU
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
{1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286}
{151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931}
EndGlobalSection
EndGlobal

3
global.json Normal file
View File

@ -0,0 +1,3 @@
{
"projects": ["src"]
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>1139bdee-fa15-474d-8855-0ab91f23cf26</ProjectGuid>
<RootNamespace>ResponseCachingSample</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2931</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,33 @@
// 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.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Net.Http.Headers;
namespace ResponseCachingSample
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCaching();
}
public void Configure(IApplicationBuilder app)
{
app.UseResponseCaching();
app.Run(async (context) =>
{
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
});
}
}
}

View File

@ -0,0 +1,33 @@
{
"webroot": "wwwroot",
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.ResponseCaching": "1.0.0-*",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.Framework.Caching.Memory": "1.0.0-*"
},
"commands": {
"web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"publishExclude": [
"node_modules",
"bower_components",
"**.xproj",
"**.user",
"**.vspscc"
],
"exclude": [
"wwwroot",
"node_modules",
"bower_components"
]
}

View File

@ -0,0 +1,143 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.Framework.Caching.Memory;
namespace Microsoft.AspNet.ResponseCaching
{
internal class CachingContext
{
private string _cacheKey;
public CachingContext(HttpContext httpContext, IMemoryCache cache)
{
HttpContext = httpContext;
Cache = cache;
}
private HttpContext HttpContext { get; }
private IMemoryCache Cache { get; }
private Stream OriginalResponseStream { get; set; }
private MemoryStream Buffer { get; set; }
internal bool ResponseStarted { get; set; }
private bool CacheResponse { get; set; }
internal bool CheckRequestAllowsCaching()
{
// Verify the method
// TODO: What other methods should be supported?
if (!string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Verify the request headers do not opt-out of caching
// TODO:
return true;
}
// Only QueryString is treated as case sensitive
// GET;HTTP://MYDOMAIN.COM:80/PATHBASE/PATH?QueryString
private string CreateCacheKey()
{
var request = HttpContext.Request;
return request.Method.ToUpperInvariant()
+ ";"
+ request.Scheme.ToUpperInvariant()
+ "://"
+ request.Host.Value.ToUpperInvariant()
+ request.PathBase.Value.ToUpperInvariant()
+ request.Path.Value.ToUpperInvariant()
+ request.QueryString;
}
internal async Task<bool> TryServeFromCacheAsync()
{
_cacheKey = CreateCacheKey();
ResponseCacheEntry cacheEntry;
if (Cache.TryGetValue(_cacheKey, out cacheEntry))
{
// TODO: Compare cached request headers
// TODO: Evaluate Vary-By and select the most appropriate response
// TODO: Content negotiation if there are multiple cached response formats?
// TODO: Verify content freshness, or else re-validate the data?
var response = HttpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = cacheEntry.StatusCode;
foreach (var pair in cacheEntry.Headers)
{
response.Headers.SetValues(pair.Key, pair.Value);
}
// TODO: Update cache headers (Age)
response.Headers["Served_From_Cache"] = DateTime.Now.ToString();
// Copy the cached response body
var body = cacheEntry.Body;
if (body.Length > 0)
{
await response.Body.WriteAsync(body, 0, body.Length);
}
return true;
}
return false;
}
internal void HookResponseStream()
{
// TODO: Use a wrapper stream to listen for writes (e.g. the start of the response),
// check the headers, and verify if we should cache the response.
// Then we should stream data out to the client at the same time as we buffer for the cache.
// For now we'll just buffer everything in memory before checking the response headers.
// TODO: Consider caching large responses on disk and serving them from there.
OriginalResponseStream = HttpContext.Response.Body;
Buffer = new MemoryStream();
HttpContext.Response.Body = Buffer;
}
internal bool OnResponseStarting()
{
// Evaluate the response headers, see if we should buffer and cache
CacheResponse = true; // TODO:
return CacheResponse;
}
internal void FinalizeCaching()
{
if (CacheResponse)
{
// Store the buffer to cache
var cacheEntry = new ResponseCacheEntry();
cacheEntry.StatusCode = HttpContext.Response.StatusCode;
cacheEntry.Headers = HttpContext.Response.Headers.ToList();
cacheEntry.Body = Buffer.ToArray();
Cache.Set(_cacheKey, cacheEntry); // TODO: Timeouts
}
// TODO: TEMP, flush the buffer to the client
Buffer.Seek(0, SeekOrigin.Begin);
Buffer.CopyTo(OriginalResponseStream);
}
internal void UnhookResponseStream()
{
// Unhook the response stream.
HttpContext.Response.Body = OriginalResponseStream;
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>d1031270-dbd3-4f02-a3dc-3e7dade8ebe6</ProjectGuid>
<RootNamespace>Microsoft.AspNet.ResponseCaching</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,8 @@
// 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.Reflection;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNet.ResponseCaching.Tests")]
[assembly: AssemblyMetadata("Serviceable", "True")]

View File

@ -0,0 +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.Collections.Generic;
namespace Microsoft.AspNet.ResponseCaching
{
internal class ResponseCacheEntry
{
public int StatusCode { get; set; }
internal IEnumerable<KeyValuePair<string, string[]>> Headers { get; set; }
internal byte[] Body { get; set; }
}
}

View File

@ -0,0 +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 Microsoft.AspNet.ResponseCaching;
namespace Microsoft.AspNet.Builder
{
public static class ResponseCachingExtensions
{
public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
{
return app.UseMiddleware<ResponseCachingMiddleware>();
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.Framework.Caching.Memory;
namespace Microsoft.AspNet.ResponseCaching
{
// http://tools.ietf.org/html/rfc7234
public class ResponseCachingMiddleware
{
private readonly RequestDelegate _next;
private readonly IMemoryCache _cache;
public ResponseCachingMiddleware(RequestDelegate next, IMemoryCache cache)
{
_next = next;
_cache = cache;
}
public async Task Invoke(HttpContext context)
{
var cachingContext = new CachingContext(context, _cache);
// Should we attempt any caching logic?
if (cachingContext.CheckRequestAllowsCaching())
{
// Can this request be served from cache?
if (await cachingContext.TryServeFromCacheAsync())
{
return;
}
// Hook up to listen to the response stream
cachingContext.HookResponseStream();
try
{
await _next(context);
// If there was no response body, check the response headers now. We can cache things like redirects.
if (!cachingContext.ResponseStarted)
{
cachingContext.OnResponseStarting();
}
// Finalize the cache entry
cachingContext.FinalizeCaching();
}
finally
{
cachingContext.UnhookResponseStream();
}
}
else
{
await _next(context);
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"version": "1.0.0-*",
"description": "Middleare that automatically caches HTTP responses on the server.",
"dependencies": {
"Microsoft.AspNet.Http.Abstractions": "1.0.0-*",
"Microsoft.Framework.Caching.Abstractions": "1.0.0-*"
},
"frameworks" : {
"dnx451": { },
"dnxcore50" : {
"dependencies": {
}
}
}
}

View File

@ -0,0 +1,33 @@
// 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 Microsoft.AspNet.Http.Internal;
using Microsoft.Framework.Caching.Memory;
using Xunit;
namespace Microsoft.AspNet.ResponseCaching
{
public class CachingContextTests
{
[Fact]
public void CheckRequestAllowsCaching_Method_GET_Allowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
Assert.True(context.CheckRequestAllowsCaching());
}
[Theory]
[InlineData("POST")]
public void CheckRequestAllowsCaching_Method_Unsafe_NotAllowed(string method)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
Assert.False(context.CheckRequestAllowsCaching());
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>151b2027-3936-44b9-a4a0-e1e5902125ab</ProjectGuid>
<RootNamespace>Microsoft.AspNet.ResponseCaching.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,15 @@
{
"dependencies": {
"Microsoft.AspNet.ResponseCaching": "1.0.0-*",
"xunit.runner.aspnet": "2.0.0-aspnet-*",
"Microsoft.AspNet.Http": "1.0.0-beta5-11528",
"Microsoft.Framework.Caching.Memory": "1.0.0-beta5-11395"
},
"commands": {
"test": "xunit.runner.aspnet"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
}
}