Skip to content

Commit e3a09e0

Browse files
authored
Merge pull request filipw#252 from LoveSilense/dev
Ability to cache specified HTTP headers. (v2)
2 parents 46e553c + b916a79 commit e3a09e0

File tree

10 files changed

+454
-7
lines changed

10 files changed

+454
-7
lines changed

src/WebApi.OutputCache.Core/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ public sealed class Constants
55
public const string ContentTypeKey = ":response-ct";
66
public const string EtagKey = ":response-etag";
77
public const string GenerationTimestampKey = ":response-generationtimestamp";
8+
public const string CustomHeaders = ":custom-headers";
9+
public const string CustomContentHeaders = ":custom-content-headers";
810
}
911
}

src/WebApi.OutputCache.V2/CacheOutputAttribute.cs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net;
45
using System.Net.Http;
@@ -81,6 +82,11 @@ public int SharedTimeSpan
8182
/// </summary>
8283
public Type CacheKeyGenerator { get; set; }
8384

85+
/// <summary>
86+
/// Comma seperated list of HTTP headers to cache
87+
/// </summary>
88+
public string IncludeCustomHeaders { get; set; }
89+
8490
/// <summary>
8591
/// If set to something else than an empty string, this value will always be used for the Content-Type header, regardless of content negotiation.
8692
/// </summary>
@@ -185,6 +191,9 @@ public override void OnActionExecuting(HttpActionContext actionContext)
185191

186192
if (!_webApiCache.Contains(cachekey)) return;
187193

194+
var responseHeaders = _webApiCache.Get<Dictionary<string, List<string>>>(cachekey + Constants.CustomHeaders);
195+
var responseContentHeaders = _webApiCache.Get<Dictionary<string, List<string>>>(cachekey + Constants.CustomContentHeaders);
196+
188197
if (actionContext.Request.Headers.IfNoneMatch != null)
189198
{
190199
var etag = _webApiCache.Get<string>(cachekey + Constants.EtagKey);
@@ -194,7 +203,8 @@ public override void OnActionExecuting(HttpActionContext actionContext)
194203
{
195204
var time = CacheTimeQuery.Execute(DateTime.Now);
196205
var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified);
197-
206+
if (responseHeaders != null) AddCustomCachedHeaders(quickResponse, responseHeaders, responseContentHeaders);
207+
198208
SetEtag(quickResponse, etag);
199209
ApplyCacheHeaders(quickResponse, time);
200210
actionContext.Response = quickResponse;
@@ -225,6 +235,8 @@ public override void OnActionExecuting(HttpActionContext actionContext)
225235
var responseEtag = _webApiCache.Get<string>(cachekey + Constants.EtagKey);
226236
if (responseEtag != null) SetEtag(actionContext.Response, responseEtag);
227237

238+
if (responseHeaders != null) AddCustomCachedHeaders(actionContext.Response, responseHeaders, responseContentHeaders);
239+
228240
var cacheTime = CacheTimeQuery.Execute(DateTime.Now);
229241
ApplyCacheHeaders(actionContext.Response, cacheTime, contentGenerationTimestamp);
230242
}
@@ -275,10 +287,27 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio
275287
etag,
276288
cacheTime.AbsoluteExpiration, baseKey);
277289

278-
279290
_webApiCache.Add(cachekey + Constants.GenerationTimestampKey,
280291
actionExecutionTimestamp.ToString(),
281292
cacheTime.AbsoluteExpiration, baseKey);
293+
294+
if (!String.IsNullOrEmpty(IncludeCustomHeaders))
295+
{
296+
// convert to dictionary of lists to ensure thread safety if implementation of IEnumerable is changed
297+
var headers = actionExecutedContext.Response.Headers.Where(h => IncludeCustomHeaders.Contains(h.Key))
298+
.ToDictionary(x => x.Key, x => x.Value.ToList());
299+
300+
var contentHeaders = actionExecutedContext.Response.Content.Headers.Where(h => IncludeCustomHeaders.Contains(h.Key))
301+
.ToDictionary(x => x.Key, x => x.Value.ToList());
302+
303+
_webApiCache.Add(cachekey + Constants.CustomHeaders,
304+
headers,
305+
cacheTime.AbsoluteExpiration, baseKey);
306+
307+
_webApiCache.Add(cachekey + Constants.CustomContentHeaders,
308+
contentHeaders,
309+
cacheTime.AbsoluteExpiration, baseKey);
310+
}
282311
}
283312
}
284313
}
@@ -311,6 +340,25 @@ protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime
311340
}
312341
}
313342

343+
protected virtual void AddCustomCachedHeaders(HttpResponseMessage response, Dictionary<string, List<string>> headers, Dictionary<string, List<string>> contentHeaders)
344+
{
345+
foreach (var headerKey in headers.Keys)
346+
{
347+
foreach (var headerValue in headers[headerKey])
348+
{
349+
response.Headers.Add(headerKey, headerValue);
350+
}
351+
}
352+
353+
foreach (var headerKey in contentHeaders.Keys)
354+
{
355+
foreach (var headerValue in contentHeaders[headerKey])
356+
{
357+
response.Content.Headers.Add(headerKey, headerValue);
358+
}
359+
}
360+
}
361+
314362
protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime)
315363
{
316364
return Guid.NewGuid().ToString();

test/WebApi.OutputCache.Core.Tests/WebApi.OutputCache.Core.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
<Name>WebApi.OutputCache.Core</Name>
5757
</ProjectReference>
5858
</ItemGroup>
59+
<ItemGroup>
60+
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
61+
</ItemGroup>
5962
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
6063
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
6164
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ public void maxage_private_true_headers_correct()
122122
public void maxage_mustrevalidate_headers_correct_with_cacheuntil()
123123
{
124124
var client = new HttpClient(_server);
125-
var result = client.GetAsync(_url + "Get_until25012015_1700").Result;
126-
var clientTimeSpanSeconds = new SpecificTime(2019, 01, 25, 17, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds;
125+
var result = client.GetAsync(_url + "Get_until25012100_1700").Result;
126+
var clientTimeSpanSeconds = new SpecificTime(2100, 01, 25, 17, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds;
127127
var resultCacheControlSeconds = ((TimeSpan) result.Headers.CacheControl.MaxAge).TotalSeconds;
128128
Assert.IsTrue(Math.Round(clientTimeSpanSeconds - resultCacheControlSeconds) == 0);
129129
Assert.IsFalse(result.Headers.CacheControl.MustRevalidate);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Net.Http.Formatting;
6+
using System.Net.Http.Headers;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using System.Web.Http;
11+
using System.Web.Http.Results;
12+
13+
namespace WebApi.OutputCache.V2.Tests
14+
{
15+
public class CustomHeadersContent<T> : OkNegotiatedContentResult<T>
16+
{
17+
public string ContentDisposition { get; set; }
18+
19+
public List<string> ContentEncoding { get; set; }
20+
21+
public string RequestHeader1 { get; set; }
22+
23+
public List<string> RequestHeader2 { get; set; }
24+
25+
public CustomHeadersContent(T content, ApiController controller)
26+
: base(content, controller) { }
27+
28+
public CustomHeadersContent(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
29+
: base(content, contentNegotiator, request, formatters) { }
30+
31+
public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
32+
{
33+
HttpResponseMessage response = await base.ExecuteAsync(cancellationToken);
34+
35+
if (!string.IsNullOrWhiteSpace(ContentDisposition))
36+
{
37+
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(ContentDisposition);
38+
}
39+
if (ContentEncoding != null)
40+
{
41+
foreach (var contentEncoding in ContentEncoding)
42+
{
43+
response.Content.Headers.ContentEncoding.Add(contentEncoding);
44+
}
45+
}
46+
47+
if (!string.IsNullOrWhiteSpace(RequestHeader1))
48+
{
49+
response.Headers.Add("RequestHeader1", RequestHeader1);
50+
}
51+
if (RequestHeader2 != null)
52+
{
53+
foreach (var requestHeader2Value in RequestHeader2)
54+
{
55+
response.Headers.Add("RequestHeader2", requestHeader2Value);
56+
}
57+
}
58+
59+
return response;
60+
}
61+
}
62+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Net.Http.Headers;
5+
using System.Security.Principal;
6+
using System.Threading;
7+
using System.Web.Http;
8+
using Autofac;
9+
using Autofac.Integration.WebApi;
10+
using Moq;
11+
using NUnit.Framework;
12+
using WebApi.OutputCache.Core;
13+
using WebApi.OutputCache.Core.Cache;
14+
using System.Collections.Generic;
15+
using System.Linq;
16+
17+
namespace WebApi.OutputCache.V2.Tests
18+
{
19+
[TestFixture]
20+
public class CustomHeadersTests
21+
{
22+
private HttpServer _server;
23+
private string _url = "http://www.strathweb.com/api/customheaders/";
24+
private IApiOutputCache _cache;
25+
26+
[SetUp]
27+
public void init()
28+
{
29+
Thread.CurrentPrincipal = null;
30+
31+
_cache = new SimpleCacheForTests();
32+
33+
var conf = new HttpConfiguration();
34+
var builder = new ContainerBuilder();
35+
builder.RegisterInstance(_cache);
36+
37+
conf.DependencyResolver = new AutofacWebApiDependencyResolver(builder.Build());
38+
conf.Routes.MapHttpRoute(
39+
name: "DefaultApi",
40+
routeTemplate: "api/{controller}/{action}/{id}",
41+
defaults: new { id = RouteParameter.Optional }
42+
);
43+
44+
_server = new HttpServer(conf);
45+
}
46+
47+
[Test]
48+
public void cache_custom_content_header() {
49+
var client = new HttpClient(_server);
50+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header");
51+
var result = client.SendAsync(req).Result;
52+
53+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header");
54+
var result2 = client.SendAsync(req2).Result;
55+
56+
Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment"));
57+
Assert.That(result2.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment"));
58+
}
59+
60+
[Test]
61+
public void cache_custom_content_header_with_multiply_values()
62+
{
63+
var client = new HttpClient(_server);
64+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header_Multiply_Values");
65+
var result = client.SendAsync(req).Result;
66+
67+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Content_Header_Multiply_Values");
68+
var result2 = client.SendAsync(req2).Result;
69+
70+
Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
71+
Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
72+
Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
73+
74+
Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
75+
Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
76+
Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
77+
}
78+
79+
[Test]
80+
public void cache_custom_response_header()
81+
{
82+
var client = new HttpClient(_server);
83+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header");
84+
var result = client.SendAsync(req).Result;
85+
86+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header");
87+
var result2 = client.SendAsync(req2).Result;
88+
89+
Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1"));
90+
Assert.That(result2.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1"));
91+
}
92+
93+
[Test]
94+
public void cache_custom_response_header_with_multiply_values()
95+
{
96+
var client = new HttpClient(_server);
97+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header_Multiply_Values");
98+
var result = client.SendAsync(req).Result;
99+
100+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Custom_Response_Header_Multiply_Values");
101+
var result2 = client.SendAsync(req2).Result;
102+
103+
Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2));
104+
Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2"));
105+
Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3"));
106+
107+
Assert.That(result2.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2));
108+
Assert.That(result2.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2"));
109+
Assert.That(result2.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3"));
110+
}
111+
112+
[Test]
113+
public void cache_multiply_custom_headers()
114+
{
115+
var client = new HttpClient(_server);
116+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Multiply_Custom_Headers");
117+
var result = client.SendAsync(req).Result;
118+
119+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Multiply_Custom_Headers");
120+
var result2 = client.SendAsync(req2).Result;
121+
122+
Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment"));
123+
Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
124+
Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
125+
Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
126+
Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1"));
127+
Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2));
128+
Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2"));
129+
Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3"));
130+
131+
Assert.That(result2.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment"));
132+
Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
133+
Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
134+
Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
135+
Assert.That(result2.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1"));
136+
Assert.That(result2.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2));
137+
Assert.That(result2.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2"));
138+
Assert.That(result2.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3"));
139+
}
140+
141+
[Test]
142+
public void cache_part_of_custom_headers()
143+
{
144+
var client = new HttpClient(_server);
145+
var req = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Part_Of_Custom_Headers");
146+
var result = client.SendAsync(req).Result;
147+
148+
var req2 = new HttpRequestMessage(HttpMethod.Get, _url + "Cache_Part_Of_Custom_Headers");
149+
var result2 = client.SendAsync(req2).Result;
150+
151+
Assert.That(result.Content.Headers.ContentDisposition.DispositionType, Is.EqualTo("attachment"));
152+
Assert.That(result.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
153+
Assert.That(result.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
154+
Assert.That(result.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
155+
Assert.That(result.Headers.GetValues("RequestHeader1").First(), Is.EqualTo("value1"));
156+
Assert.That(result.Headers.GetValues("RequestHeader2").Count(), Is.EqualTo(2));
157+
Assert.That(result.Headers.GetValues("RequestHeader2").First(), Is.EqualTo("value2"));
158+
Assert.That(result.Headers.GetValues("RequestHeader2").Last(), Is.EqualTo("value3"));
159+
160+
Assert.That(result2.Content.Headers.ContentDisposition, Is.Null);
161+
Assert.That(result2.Content.Headers.ContentEncoding.Count, Is.EqualTo(2));
162+
Assert.That(result2.Content.Headers.ContentEncoding.First(), Is.EqualTo("deflate"));
163+
Assert.That(result2.Content.Headers.ContentEncoding.Last(), Is.EqualTo("gzip"));
164+
165+
IEnumerable<string> headerValue = null;
166+
Assert.That(result2.Headers.TryGetValues("RequestHeader1", out headerValue), Is.False);
167+
Assert.That(result2.Headers.TryGetValues("RequestHeader2", out headerValue), Is.False);
168+
}
169+
170+
[TearDown]
171+
public void fixture_dispose()
172+
{
173+
if (_server != null) _server.Dispose();
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)