Skip to content

Commit

Permalink
Added Glenn Block's ETagHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisMissal authored and pedroreys committed Apr 4, 2012
1 parent 93910d4 commit 6e3915d
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 7 deletions.
89 changes: 89 additions & 0 deletions src/WebApiContrib/MessageHandlers/ETagHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using WebApiContrib.ResponseMessages;

namespace WebApiContrib.MessageHandlers
{
// Applied from http://codepaste.net/4w6c6i - by Glenn Block
public class ETagHandler : DelegatingHandler
{
public static ConcurrentDictionary<string, EntityTagHeaderValue> ETagCache = new ConcurrentDictionary<string, EntityTagHeaderValue>();

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Method == HttpMethod.Get)
{
// should we ignore trailing slash
var resource = request.RequestUri.ToString();

ICollection<EntityTagHeaderValue> etags = request.Headers.IfNoneMatch;
// compare the Etag with the one in the cache
// do conditional get.
EntityTagHeaderValue actualEtag = null;
if (etags.Count > 0 && ETagHandler.ETagCache.TryGetValue(resource, out actualEtag))
{
if (etags.Any(etag => etag.Tag == actualEtag.Tag))
{
return NotModifiedResponse(cancellationToken);
}
}
}
else if (request.Method == HttpMethod.Put)
{
// should we ignore trailing slash
var resource = request.RequestUri.ToString();

ICollection<EntityTagHeaderValue> etags = request.Headers.IfMatch;
// compare the Etag with the one in the cache
// do conditional get.

EntityTagHeaderValue actualEtag = null;
if (etags.Count > 0 && ETagHandler.ETagCache.TryGetValue(resource, out actualEtag))
{
var matchFound = etags.Any(etag => etag.Tag == actualEtag.Tag);

if (!matchFound)
{
return ConflictResponse(cancellationToken);
}
}
}
return base.SendAsync(request, cancellationToken).ContinueWith(task =>
{
var httpResponse = task.Result;
var eTagKey = request.RequestUri.ToString();
EntityTagHeaderValue eTagValue;
// Post would invalidate the collection, put should invalidate the individual item
if (!ETagCache.TryGetValue(eTagKey, out eTagValue) || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post)
{
eTagValue = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");
ETagCache.AddOrUpdate(eTagKey, eTagValue, (key, existingVal) => eTagValue);
}
httpResponse.Headers.ETag = eTagValue;
return httpResponse;
});
}

private static Task<HttpResponseMessage> NotModifiedResponse(CancellationToken cancellationToken)
{
var response = new NotModifiedResponse { Content = new StringContent("The resource is not modified") };

return Task.Factory.StartNew<HttpResponseMessage>(task => response, cancellationToken);
}

private static Task<HttpResponseMessage> ConflictResponse(CancellationToken cancellationToken)
{
var response = new ConflictResponse { Content = new StringContent("If-Match header value is different from the ETag") };

return Task.Factory.StartNew<HttpResponseMessage>(task => response, cancellationToken);
}
}
}
27 changes: 27 additions & 0 deletions src/WebApiContrib/ResponseMessages/ConflictResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http;

namespace WebApiContrib.ResponseMessages
{
public class ConflictResponse : ResourceResponseBase
{
public ConflictResponse() : base(HttpStatusCode.Conflict)
{
}

public ConflictResponse(IApiResource apiResource) : base(HttpStatusCode.Conflict, apiResource)
{
}
}

public class ConflictResponse<T> : HttpResponseMessage<T>
{
public ConflictResponse() : base(HttpStatusCode.Conflict)
{
}

public ConflictResponse(T resource) : base(resource, HttpStatusCode.Conflict)
{
}
}
}
10 changes: 6 additions & 4 deletions src/WebApiContrib/WebApiContrib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Formatting\JsonpFormatter.cs" />
<Compile Include="Internal\MediaTypeConstants.cs" />
<Compile Include="Internal\MediaTypeConstants.cs" />
<Compile Include="Conneg\ContentNegotiation.cs" />
<Compile Include="Content\CompressedContent.cs" />
<Compile Include="Content\SimpleObjectContent.cs" />
Expand All @@ -77,19 +77,21 @@
<Compile Include="Internal\Extensions\EnumerableExtensions.cs" />
<Compile Include="Internal\ReflectionHelper.cs" />
<Compile Include="MessageHandlers\EncodingHandler.cs" />
<Compile Include="MessageHandlers\ETagHandler.cs" />
<Compile Include="MessageHandlers\LoggingHandler.cs" />
<Compile Include="MessageHandlers\MethodOverrideHandler.cs" />
<Compile Include="MessageHandlers\NotAcceptableMessageHandler.cs" />
<Compile Include="MessageHandlers\NotAcceptableMessageHandler.cs" />
<Compile Include="MessageHandlers\SimpleCorsHandler.cs" />
<Compile Include="MessageHandlers\UriExtensionMappings.cs" />
<Compile Include="MessageHandlers\UriFormatExtensionHandler.cs" />
<Compile Include="Messages\ApiLoggingInfo.cs" />
<Compile Include="Messages\ApiLoggingInfo.cs" />
<Compile Include="MessageHandlers\HeadMessageHandler.cs" />
<Compile Include="MessageHandlers\HttpMethodTunnelMessageHandler.cs" />
<Compile Include="MessageHandlers\HttpMethodTunnelMessageHandler.cs" />
<Compile Include="MessageHandlers\SelfHostConsoleOutputHandler.cs" />
<Compile Include="MessageHandlers\TraceMessageHandler.cs" />
<Compile Include="MessageHandlers\UriFormatExtensionMessageHandler.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ResponseMessages\ConflictResponse.cs" />
<Compile Include="ResponseMessages\CreateResponse.cs" />
<Compile Include="ResponseMessages\MovedPermanentlyResponse.cs" />
<Compile Include="ResponseMessages\OkResponse.cs" />
Expand Down
88 changes: 88 additions & 0 deletions test/WebApiContribTests/MessageHandlers/ETagHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using NUnit.Framework;
using Should;
using WebApiContrib.MessageHandlers;

namespace WebApiContribTests.MessageHandlers
{
[TestFixture]
public class ETagHandlerTests : MessageHandlerTester
{
private KeyValuePair<string, EntityTagHeaderValue> etag = new KeyValuePair<string, EntityTagHeaderValue>("foo/bar", new EntityTagHeaderValue("\"" + Guid.NewGuid() + "\""));

[Test]
public void Should_return_NotModified_if_ETag_is_found_in_the_cache()
{
var eTagHandler = GetHandler();

var requestMessage = new HttpRequestMessage(HttpMethod.Get, etag.Key);
requestMessage.Headers.Add("If-None-Match", etag.Value.Tag);
AddETagValue(etag);
var response = ExecuteRequest(eTagHandler, requestMessage);

response.StatusCode.ShouldEqual(HttpStatusCode.NotModified);
}

[Test]
public void Should_return_Conflict_for_Put_if_key_is_in_header_but_match_not_found_in_cache()
{
var eTagHandler = GetHandler();

var requestMessage = new HttpRequestMessage(HttpMethod.Put, etag.Key);
requestMessage.Headers.Add("If-Match", etag.Value.Tag);

var newRandomValue = new EntityTagHeaderValue("\"" + Guid.NewGuid() + "\"");
AddETagValue(new KeyValuePair<string, EntityTagHeaderValue>(etag.Key, newRandomValue));

var response = ExecuteRequest(eTagHandler, requestMessage);

response.StatusCode.ShouldEqual(HttpStatusCode.Conflict);
}

[Test]
public void Post_should_return_OK_and_ETag_in_Header_and_update_Cache_if_ETag_is_found_in_the_cache()
{
var eTagHandler = GetHandler();

var requestMessage = new HttpRequestMessage(HttpMethod.Post, "foo/bar");
requestMessage.Headers.Add("If-None-Match", etag.Value.Tag);
AddETagValue(etag);
ETagHandler.ETagCache.Count.ShouldEqual(1);
var response = ExecuteRequest(eTagHandler, requestMessage);

response.StatusCode.ShouldEqual(HttpStatusCode.OK);
response.Headers.ETag.ShouldNotBeNull();
ETagHandler.ETagCache.Count.ShouldEqual(1);
}

[Test]
public void Get_should_return_OK_and_ETag_in_Header_and_add_to_Cache_if_ETag_is_not_found_in_the_cache()
{
var eTagHandler = GetHandler();

var requestMessage = new HttpRequestMessage(HttpMethod.Get, "foo/bar");
requestMessage.Headers.Add("If-None-Match", etag.Value.Tag);
// not added to cache
ETagHandler.ETagCache.Count.ShouldEqual(0);
var response = ExecuteRequest(eTagHandler, requestMessage);

response.StatusCode.ShouldEqual(HttpStatusCode.OK);
response.Headers.ETag.ShouldNotBeNull();
ETagHandler.ETagCache.Count.ShouldEqual(1);
}

private static void AddETagValue(KeyValuePair<string, EntityTagHeaderValue> pair)
{
ETagHandler.ETagCache.AddOrUpdate(pair.Key, pair.Value, (key, val) => pair.Value);
}

private ETagHandler GetHandler()
{
return new ETagHandler();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ public HttpResponseMessage ExecuteRequest(DelegatingHandler testTarget, HttpRequ

var requestTask = SendAsync(requestMessage, new CancellationToken());

while (!requestTask.IsCompleted)
{
}
requestTask.Wait(5000); // 5 second timeout - tests should be quicker than this, but better than infinite for now

return requestTask.Result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Net;
using NUnit.Framework;
using Should;
using WebApiContrib.ResponseMessages;

namespace WebApiContribTests.ResponseMessages
{
[TestFixture]
public class ConflictResponseMessageTests : HttpResponseMessageTester
{
[Test]
public void Should_return_an_http_response_message_with_expected_status()
{
var response = new ConflictResponse();

AssertExpectedStatus(response);
}

[Test]
public void Should_add_location_header_to_the_message_when_response_contains_a_api_resource()
{
var apiResource = new TestResource();

var response = new ConflictResponse(apiResource);
AssertExpectedStatus(response);
response.Headers.Location.ShouldEqual(apiResource.Location);
}

[Test]
public void Should_add_content_to_message_when_its_a_typed_response_message()
{
var apiResource = new TestResource();
var response = new ConflictResponse<TestResource>(apiResource);
AssertExpectedStatus(response);
response.Headers.Location.ShouldEqual(apiResource.Location);
response.Content.ShouldNotBeNull();
response.Content.ObjectType.ShouldEqual(typeof(TestResource));
}

protected override HttpStatusCode? ExpectedStatusCode
{
get { return HttpStatusCode.Conflict; }
}
}
}
2 changes: 2 additions & 0 deletions test/WebApiContribTests/WebApiContribTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@
<Compile Include="Helpers\PrecannedMessageHandler.cs" />
<Compile Include="IoC\DependencyInjectionTests.cs" />
<Compile Include="MessageHandlers\EncodingHandlerTests.cs" />
<Compile Include="MessageHandlers\ETagHandlerTests.cs" />
<Compile Include="MessageHandlers\HeadMessageHandlerTests.cs" />
<Compile Include="MessageHandlers\LoggingHandlerTests.cs" />
<Compile Include="MessageHandlers\MessageHandlerTester.cs" />
<Compile Include="MessageHandlers\NotAcceptableMessageHandlerTests.cs" />
<Compile Include="ResponseMessages\ConflictResponseMessageTests.cs" />
<Compile Include="ResponseMessages\CreateResponseMessageTests.cs" />
<Compile Include="ResponseMessages\HttpResponseMessageTester.cs" />
<Compile Include="ResponseMessages\NotModifiedResponseTests.cs" />
Expand Down

0 comments on commit 6e3915d

Please sign in to comment.