public static void TestCachingContinuation( string method, bool existsInStore, bool addVaryHeader, bool addLastModifiedHeader, bool alreadyHasLastModified, string[] varyByHeader) { // setup var mocks = new MockRepository(); var request = new HttpRequestMessage(new HttpMethod(method), TestUrl); request.Headers.Add(HttpHeaderNames.Accept, "text/xml"); request.Headers.Add(HttpHeaderNames.AcceptLanguage, "en-GB"); var entityTagStore = mocks.StrictMock<IEntityTagStore>(); var cachingHandler = new CachingHandler(entityTagStore, varyByHeader) { AddLastModifiedHeader = addLastModifiedHeader, AddVaryHeader = addVaryHeader }; var entityTagHeaderValue = new TimedEntityTagHeaderValue("\"12345678\""); var entityTagKey = new EntityTagKey(TestUrl, new string[0]); cachingHandler.EntityTagKeyGenerator = (x, y) => entityTagKey; cachingHandler.ETagValueGenerator = (x, y) => new EntityTagHeaderValue(entityTagHeaderValue.Tag); entityTagStore.Expect(x => x.TryGet(Arg<EntityTagKey>.Matches(etg => etg.ResourceUri == TestUrl), out Arg<TimedEntityTagHeaderValue>.Out(entityTagHeaderValue).Dummy)).Return(existsInStore); if (!existsInStore || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) { entityTagStore.Expect( x => x.AddOrUpdate(Arg<EntityTagKey>.Matches(etk => etk == entityTagKey), Arg<TimedEntityTagHeaderValue>.Matches(ethv => ethv.Tag == entityTagHeaderValue.Tag))); } mocks.ReplayAll(); var response = new HttpResponseMessage(); response.Content = new ByteArrayContent(new byte[0]); if(alreadyHasLastModified) response.Content.Headers.Add(HttpHeaderNames.LastModified, DateTimeOffset.Now.ToString("r")); var cachingContinuation = cachingHandler.GetCachingContinuation(request); var taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>(); taskCompletionSource.SetResult(response); // run var httpResponseMessage = cachingContinuation(taskCompletionSource.Task); // test if(addLastModifiedHeader) { Assert.That(httpResponseMessage.Content.Headers.Any(x=>x.Key == HttpHeaderNames.LastModified), "LastModified does not exist"); } if(!addLastModifiedHeader && !alreadyHasLastModified) { Assert.That(!httpResponseMessage.Content.Headers.Any(x => x.Key == HttpHeaderNames.LastModified), "LastModified exists"); } }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > PutIfMatch() { return((request) => { if (request.Method != HttpMethod.Put) { return null; } ICollection <EntityTagHeaderValue> matchTags = request.Headers.IfMatch; if (matchTags == null || matchTags.Count == 0) { return null; } var headers = request.Headers.Where(h => _varyByHeaders.Any(v => v.Equals(h.Key, StringComparison.CurrentCultureIgnoreCase))); var resource = UriTrimmer(request.RequestUri); var entityTagKey = EntityTagKeyGenerator(resource, headers); TimedEntityTagHeaderValue actualEtag = null; bool matchFound = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { if (matchTags.Any(etag => etag.Tag == actualEtag.Tag)) { matchFound = true; } } return matchFound ? null : new HttpResponseMessage(HttpStatusCode.PreconditionFailed).ToTask(); }); }
public static void GetMatchNonMatchTest( string headerName, string[] values, bool existsInStore, bool expectReturnNull, HttpStatusCode expectedStatus = HttpStatusCode.Unused) { // setup var mocks = new MockRepository(); var entityTagStore = mocks.StrictMock<IEntityTagStore>(); var entityTagHandler = new CachingHandler(entityTagStore); var request = new HttpRequestMessage(HttpMethod.Get, TestUrl); request.Headers.Add(headerName, values); TimedEntityTagHeaderValue entityTagHeaderValue = new TimedEntityTagHeaderValue("\"12345678\""); if(values.Length>0) // if entityTagStore.Expect(x => x.TryGetValue(Arg<EntityTagKey>.Matches(etg => etg.ResourceUri == entityTagHandler.UriTrimmer(new Uri(TestUrl))), out Arg<TimedEntityTagHeaderValue>.Out(entityTagHeaderValue).Dummy)).Return(existsInStore); mocks.ReplayAll(); // run var matchNoneMatch = entityTagHandler.GetIfMatchNoneMatch(); // verify Task<HttpResponseMessage> resultTask = matchNoneMatch(request); Assert.That(expectReturnNull ^ resultTask != null, "result was not as expected"); if(resultTask!=null && expectedStatus != HttpStatusCode.Unused) { Assert.AreEqual(expectedStatus, resultTask.Result.StatusCode, "Status code"); } mocks.VerifyAll(); }
public static bool TryParse(string timedETagValue, out TimedEntityTagHeaderValue value) { value = null; if (timedETagValue == null) return false; var strings = timedETagValue.Split(new[] { "\r\n" }, StringSplitOptions.None); if (strings.Length != 2) return false; EntityTagHeaderValue etag = null; DateTimeOffset lastModified; if (!EntityTagHeaderValue.TryParse(strings[0], out etag)) return false; if (!DateTimeOffset.TryParse(strings[1], out lastModified)) return false; value = new TimedEntityTagHeaderValue(etag.Tag, etag.IsWeak) { LastModified = lastModified }; return true; }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > PutIfUnmodifiedSince() { return((request) => { if (request.Method != HttpMethod.Put) { return null; } DateTimeOffset?ifUnmodifiedSince = request.Headers.IfUnmodifiedSince; if (ifUnmodifiedSince == null) { return null; } DateTimeOffset modifiedInQuestion = ifUnmodifiedSince.Value; var headers = request.Headers.Where(h => _varyByHeaders.Any(v => v.Equals(h.Key, StringComparison.CurrentCultureIgnoreCase))); var resource = UriTrimmer(request.RequestUri); var entityTagKey = EntityTagKeyGenerator(resource, headers); TimedEntityTagHeaderValue actualEtag = null; bool isModified = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { isModified = actualEtag.LastModified > modifiedInQuestion; } return isModified ? new HttpResponseMessage(HttpStatusCode.PreconditionFailed).ToTask() : null; }); }
public static bool TryParse(string timedETagValue, out TimedEntityTagHeaderValue value) { value = null; if (timedETagValue == null) { return(false); } var strings = timedETagValue.Split(new[] { "\r\n" }, StringSplitOptions.None); if (strings.Length != 2) { return(false); } EntityTagHeaderValue etag = null; DateTimeOffset lastModified; if (!EntityTagHeaderValue.TryParse(strings[0], out etag)) { return(false); } if (!DateTimeOffset.TryParse(strings[1], out lastModified)) { return(false); } value = new TimedEntityTagHeaderValue(etag.Tag, etag.IsWeak) { LastModified = lastModified }; return(true); }
public void AddOrUpdate(EntityTagKey key, TimedEntityTagHeaderValue eTag) { _eTagCache.AddOrUpdate(key, eTag, (theKey, oldValue) => eTag); _routePatternCache.AddOrUpdate(key.RoutePattern, new HashSet<EntityTagKey>() { key }, (routePattern, hashSet) => { hashSet.Add(key); return hashSet; }); }
public static void ToStringAndTryParseTest(string tag, bool isWeak) { var headerValue = new TimedEntityTagHeaderValue(tag, isWeak); var s = headerValue.ToString(); TimedEntityTagHeaderValue headerValue2 = null; Assert.IsTrue(TimedEntityTagHeaderValue.TryParse(s, out headerValue2)); Assert.AreEqual(headerValue.Tag, headerValue2.Tag); Assert.AreEqual(headerValue.LastModified.ToString(), headerValue2.LastModified.ToString()); Assert.AreEqual(headerValue.IsWeak, headerValue2.IsWeak); Assert.AreEqual(headerValue.ToString(), headerValue2.ToString()); }
/// <summary> /// Adds caching for GET and PUT if /// cache control provided is not null /// With PUT, since cache has been alreay invalidated, /// we provide the new ETag (old one has been cleared in invalidation phase) /// </summary> /// <param name="entityTagKey"></param> /// <param name="request"></param> /// <param name="response"></param> /// <param name="varyHeaders"></param> /// <returns></returns> internal Action AddCaching( EntityTagKey entityTagKey, HttpRequestMessage request, HttpResponseMessage response, IEnumerable <KeyValuePair <string, IEnumerable <string> > > varyHeaders) { return (() => { var cacheControlHeaderValue = CacheControlHeaderProvider(request); if (cacheControlHeaderValue == null) { return; } TimedEntityTagHeaderValue eTagValue; string uri = UriTrimmer(request.RequestUri); // in case of GET and no ETag // in case of PUT, we should return the new ETag of the resource // NOTE: No need to check if it is in the cache. If it were, it would not get // here if (request.Method == HttpMethod.Get || request.Method == HttpMethod.Put) { // create new ETag only if it does not already exist if (!_entityTagStore.TryGetValue(entityTagKey, out eTagValue)) { eTagValue = new TimedEntityTagHeaderValue(ETagValueGenerator(uri, varyHeaders)); _entityTagStore.AddOrUpdate(entityTagKey, eTagValue); } // set ETag response.Headers.ETag = eTagValue.ToEntityTagHeaderValue(); // set last-modified if (AddLastModifiedHeader && response.Content != null && !response.Content.Headers.Any(x => x.Key.Equals(HttpHeaderNames.LastModified, StringComparison.CurrentCultureIgnoreCase))) { response.Content.Headers.Add(HttpHeaderNames.LastModified, eTagValue.LastModified.ToString("r")); } // set Vary if (AddVaryHeader && _varyByHeaders != null && _varyByHeaders.Length > 0) { response.Headers.Add(HttpHeaderNames.Vary, _varyByHeaders); } response.Headers.TryAddWithoutValidation(HttpHeaderNames.CacheControl, cacheControlHeaderValue.ToString()); } }); }
public void AddOrUpdate(EntityTagKey key, TimedEntityTagHeaderValue eTag) { _eTagCache.AddOrUpdate(key, eTag, (theKey, oldValue) => eTag); _routePatternCache.AddOrUpdate(key.RoutePattern, new HashSet <EntityTagKey>() { key }, (routePattern, hashSet) => { hashSet.Add(key); return(hashSet); }); }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > GetIfMatchNoneMatch() { return((request) => { if (request.Method != HttpMethod.Get) { return null; } ICollection <EntityTagHeaderValue> noneMatchTags = request.Headers.IfNoneMatch; ICollection <EntityTagHeaderValue> matchTags = request.Headers.IfMatch; if (matchTags.Count == 0 && noneMatchTags.Count == 0) { return null; // no etag } if (matchTags.Count > 0 && noneMatchTags.Count > 0) // both if-match and if-none-match exist { return new HttpResponseMessage(HttpStatusCode.BadRequest).ToTask(); } var isNoneMatch = noneMatchTags.Count > 0; var etags = isNoneMatch ? noneMatchTags : matchTags; var resource = UriTrimmer(request.RequestUri); var headers = request.Headers.Where(h => _varyByHeaders.Any(v => v.Equals(h.Key, StringComparison.CurrentCultureIgnoreCase))); var entityTagKey = EntityTagKeyGenerator(resource, headers); // compare the Etag with the one in the cache // do conditional get. TimedEntityTagHeaderValue actualEtag = null; bool matchFound = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { if (etags.Any(etag => etag.Tag == actualEtag.Tag)) { matchFound = true; } } return matchFound ^ isNoneMatch ? null : new NotModifiedResponse(actualEtag.ToEntityTagHeaderValue()).ToTask(); }); }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > GetIfModifiedUnmodifiedSince() { return((request) => { if (request.Method != HttpMethod.Get && request.Headers != null) { return null; } DateTimeOffset?ifModifiedSince = request.Headers.IfModifiedSince; DateTimeOffset?ifUnmodifiedSince = request.Headers.IfUnmodifiedSince; if (ifModifiedSince == null && ifUnmodifiedSince == null) { return null; // no etag } if (ifModifiedSince != null && ifUnmodifiedSince != null) // both exist { return new HttpResponseMessage(HttpStatusCode.BadRequest).ToTask(); } bool ifModified = (ifUnmodifiedSince == null); DateTimeOffset modifiedInQuestion = ifModified ? ifModifiedSince.Value : ifUnmodifiedSince.Value; var headers = request.Headers.Where(h => _varyByHeaders.Any(v => v.Equals(h.Key, StringComparison.CurrentCultureIgnoreCase))); var resource = UriTrimmer(request.RequestUri); var entityTagKey = EntityTagKeyGenerator(resource, headers); TimedEntityTagHeaderValue actualEtag = null; bool isModified = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { isModified = actualEtag.LastModified > modifiedInQuestion; } return isModified ^ ifModified ? new NotModifiedResponse(actualEtag.ToEntityTagHeaderValue()).ToTask() : null; }); }
public static void AddCaching(string method, bool existsInStore, bool addVaryHeader, bool addLastModifiedHeader, bool alreadyHasLastModified, string[] varyByHeader) { // setup var mocks = new MockRepository(); var request = new HttpRequestMessage(new HttpMethod(method), TestUrl); request.Headers.Add(HttpHeaderNames.Accept, "text/xml"); request.Headers.Add(HttpHeaderNames.AcceptLanguage, "en-GB"); var entityTagStore = mocks.StrictMock<IEntityTagStore>(); var entityTagHeaderValue = new TimedEntityTagHeaderValue("\"12345678\""); var cachingHandler = new CachingHandler(entityTagStore, varyByHeader) { AddLastModifiedHeader = addLastModifiedHeader, AddVaryHeader = addVaryHeader, ETagValueGenerator = (x,y) => entityTagHeaderValue }; var entityTagKey = new EntityTagKey(TestUrl, new[] {"text/xml", "en-GB"}, TestUrl + "/*"); entityTagStore.Expect(x => x.TryGetValue(Arg<EntityTagKey>.Matches(etg => etg.ResourceUri == TestUrl), out Arg<TimedEntityTagHeaderValue>.Out(entityTagHeaderValue).Dummy)).Return(existsInStore); if (!existsInStore) { entityTagStore.Expect( x => x.AddOrUpdate(Arg<EntityTagKey>.Matches(etk => etk == entityTagKey), Arg<TimedEntityTagHeaderValue>.Matches(ethv => ethv.Tag == entityTagHeaderValue.Tag))); } var response = new HttpResponseMessage(); response.Content = new ByteArrayContent(new byte[0]); if (alreadyHasLastModified) response.Content.Headers.Add(HttpHeaderNames.LastModified, DateTimeOffset.Now.ToString("r")); var cachingContinuation = cachingHandler.AddCaching(entityTagKey, request, response, request.Headers); mocks.ReplayAll(); // run cachingContinuation(); // verify // test kast modified only if it is GET and PUT if (addLastModifiedHeader && method.IsIn("PUT", "GET")) { Assert.That(response.Content.Headers.Any(x => x.Key == HttpHeaderNames.LastModified), "LastModified does not exist"); } if (!addLastModifiedHeader && !alreadyHasLastModified) { Assert.That(!response.Content.Headers.Any(x => x.Key == HttpHeaderNames.LastModified), "LastModified exists"); } mocks.VerifyAll(); }
public bool TryGetValue(EntityTagKey key, out TimedEntityTagHeaderValue eTag) { return(_eTagCache.TryGetValue(key, out eTag)); }
public static void GetModifiedNotModifiedTest( string headerName, bool resourceHasChanged, bool expectReturnNull, HttpStatusCode expectedStatus = HttpStatusCode.Unused) { // setup var mocks = new MockRepository(); var entityTagStore = mocks.StrictMock<IEntityTagStore>(); var entityTagHandler = new CachingHandler(entityTagStore); var request = new HttpRequestMessage(HttpMethod.Get, TestUrl); DateTimeOffset lastChanged = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(7)); DateTimeOffset lastModifiedInQuestion = resourceHasChanged ? lastChanged.Subtract(TimeSpan.FromDays(1)) : lastChanged.Add(TimeSpan.FromDays(1)); request.Headers.Add(headerName, lastModifiedInQuestion.ToString("r")); var entityTagHeaderValue = new TimedEntityTagHeaderValue("\"12345678\"") {LastModified = lastChanged}; entityTagStore.Expect(x => x.TryGetValue(Arg<EntityTagKey>.Matches(etg => etg.ResourceUri == entityTagHandler.UriTrimmer(new Uri(TestUrl))), out Arg<TimedEntityTagHeaderValue>.Out(entityTagHeaderValue).Dummy)).Return(true); mocks.ReplayAll(); // run var modifiedUnmodifiedSince = entityTagHandler.GetIfModifiedUnmodifiedSince(); var task = modifiedUnmodifiedSince(request); HttpResponseMessage response = task == null ? null : task.Result; // verify Assert.That(expectReturnNull ^ task != null, "result was not as expected"); if (task != null && expectedStatus != HttpStatusCode.Unused) { Assert.AreEqual(expectedStatus, response.StatusCode, "Status code"); } mocks.VerifyAll(); }
internal Func<Task<HttpResponseMessage>, HttpResponseMessage> GetCachingContinuation(HttpRequestMessage request) { return task => { var httpResponse = task.Result; var cacheControlHeaderValue = CacheController(request); if (cacheControlHeaderValue == null) return httpResponse; string uri = request.RequestUri.ToString(); var varyHeaders = request.Headers.Where(h => _varyByHeaders.Any(v => v.Equals(h.Key, StringComparison.CurrentCultureIgnoreCase))); var eTagKey = EntityTagKeyGenerator(uri, varyHeaders); TimedEntityTagHeaderValue eTagValue; if (!_entityTagStore.TryGet(eTagKey, out eTagValue) || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post) { eTagValue = new TimedEntityTagHeaderValue(ETagValueGenerator(uri, varyHeaders)); _entityTagStore.AddOrUpdate(eTagKey, eTagValue ); } // set ETag httpResponse.Headers.ETag = eTagValue.ToEntityTagHeaderValue(); // set last-modified if (AddLastModifiedHeader && httpResponse.Content != null && !httpResponse.Content.Headers.Any(x=>x.Key.Equals(HttpHeaderNames.LastModified, StringComparison.CurrentCultureIgnoreCase))) { httpResponse.Content.Headers.Add(HttpHeaderNames.LastModified, eTagValue.LastModified.ToString("r")); } // set Vary if(AddVaryHeader && _varyByHeaders!=null && _varyByHeaders.Length>0) { httpResponse.Headers.Add(HttpHeaderNames.Vary, _varyByHeaders); } httpResponse.Headers.AddWithoutValidation(HttpHeaderNames.CacheControl,cacheControlHeaderValue.ToString()); return httpResponse; }; }
/// <summary> /// Adds caching for GET and PUT if /// cache control provided is not null /// With PUT, since cache has been alreay invalidated, /// we provide the new ETag (old one has been cleared in invalidation phase) /// </summary> /// <param name="entityTagKey"></param> /// <param name="request"></param> /// <param name="response"></param> /// <param name="varyHeaders"></param> /// <returns></returns> internal Action AddCaching( EntityTagKey entityTagKey, HttpRequestMessage request, HttpResponseMessage response, IEnumerable<KeyValuePair<string, IEnumerable<string>>> varyHeaders) { return () => { var cacheControlHeaderValue = CacheControlHeaderProvider(request); if (cacheControlHeaderValue == null) return; TimedEntityTagHeaderValue eTagValue; string uri = UriTrimmer(request.RequestUri); // in case of GET and no ETag // in case of PUT, we should return the new ETag of the resource // NOTE: No need to check if it is in the cache. If it were, it would not get // here if (request.Method == HttpMethod.Get || request.Method == HttpMethod.Put) { // create new ETag only if it does not already exist if (!_entityTagStore.TryGetValue(entityTagKey, out eTagValue)) { eTagValue = new TimedEntityTagHeaderValue(ETagValueGenerator(uri, varyHeaders)); _entityTagStore.AddOrUpdate(entityTagKey, eTagValue); } // set ETag response.Headers.ETag = eTagValue.ToEntityTagHeaderValue(); // set last-modified if (AddLastModifiedHeader && response.Content != null && !response.Content.Headers.Any(x => x.Key.Equals(HttpHeaderNames.LastModified, StringComparison.CurrentCultureIgnoreCase))) { response.Content.Headers.Add(HttpHeaderNames.LastModified, eTagValue.LastModified.ToString("r")); } // set Vary if (AddVaryHeader && _varyByHeaders != null && _varyByHeaders.Length > 0) { response.Headers.Add(HttpHeaderNames.Vary, _varyByHeaders); } response.Headers.TryAddWithoutValidation(HttpHeaderNames.CacheControl, cacheControlHeaderValue.ToString()); } }; }
public void AddOrUpdate(EntityTagKey key, TimedEntityTagHeaderValue eTag) { _eTagCache.AddOrUpdate(key, eTag, (theKey, oldValue) => eTag); }
public bool TryGet(EntityTagKey key, out TimedEntityTagHeaderValue eTag) { return _eTagCache.TryGetValue(key, out eTag); }