A cache item object that keeps some details about the data to be cached
Inheritance: CacheItem
        public async Task Should_call_reborn_on_existing_phoenix_and_try_refresh_cache_when_cache_item_is_stale()
        {
            // Arrange
            var store = Substitute.For<IAsyncCacheStore>();
            store.StoreId.Returns(1000);
            var objCacheItem = new WebApiCacheItem
            {
                MaxAge = 5,
                StaleWhileRevalidate = 5,
                StoreId = 1000,
                CreatedTime = DateTime.UtcNow.AddSeconds(-5).AddMilliseconds(-1),
            };

            var existingPhoenix = Substitute.For<WebApiPhoenix>(_invocation, objCacheItem, _request);
            existingPhoenix.When(x => x.Reborn()).Do(c =>
            {
                Global.Cache.PhoenixFireCage.Remove(objCacheItem.Key);
            });

            store.GetAsync(Arg.Any<string>()).Returns(c =>
            {
                objCacheItem.Key = c.Arg<string>();
                Global.Cache.PhoenixFireCage[objCacheItem.Key] = existingPhoenix;
                return Task.FromResult((object)objCacheItem);
            });

            Global.CacheStoreProvider.RegisterAsyncStore(store);
            var att = new Flatwhite.WebApi.OutputCacheAttribute { MaxAge = 5, CacheStoreId = 1000, StaleWhileRevalidate = 5 };

            // Action
            await att.OnActionExecutingAsync(_actionContext, CancellationToken.None);

            // Assert
            existingPhoenix.Received(1).Reborn();
        }
        public void Should_dispose_existing_phoenix()
        {
            var key = "theCacheKey" + Guid.NewGuid();
            // Arrange
            var objCacheItem = new WebApiCacheItem
            {
                MaxAge = 5,
                StaleWhileRevalidate = 5,
                StoreId = 1000,
                CreatedTime = DateTime.UtcNow.AddSeconds(-5).AddMilliseconds(-1),
                Key = key
            };

            var existingPhoenix = Substitute.For<WebApiPhoenix>(_invocation, objCacheItem, _request);

            var att = new OutputCacheAttributeWithPublicMethods {MaxAge = 5, CacheStoreId = 1000, StaleWhileRevalidate = 5};

            Global.Cache.PhoenixFireCage[key] = existingPhoenix;

            // Action
            att.CreatePhoenixPublic(_invocation, objCacheItem, _request);

            // Assert
            Assert.That(Global.Cache.PhoenixFireCage[key] is WebApiPhoenix);
            existingPhoenix.Received(1).Dispose();
        }
        public async Task FireAsync_should_send_a_request_to_original_endpoint_when_loopback_is_not_set()
        {
            // Arrange
            var currentCacheItem = new WebApiCacheItem
            {
                CreatedTime = DateTime.UtcNow,
                MaxAge = 2,
                StaleWhileRevalidate = 3,
                StaleIfError = 4,
                Key = "1"
            };
            var invocation = Substitute.For<_IInvocation>();


            using (var phoenix = new WebApiPhoenixWithPublicMethods(invocation, currentCacheItem, _requestMessage))
            { 
                phoenix.HttpClient = _client;
            

                // Action
                var state = await phoenix.FireAsyncPublic();

                // Assert
                Assert.IsTrue(state is InActivePhoenix);
                await _client
                    .Received(1)
                    .SendAsync(Arg.Is<HttpRequestMessage>(msg => msg.Properties.Count == 0 && 
                                                                 msg.Headers.CacheControl.Extensions.Any(e => e.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh) &&
                                                                 msg.RequestUri.ToString() == "http://localhost/api/method/id")
                                , HttpCompletionOption.ResponseHeadersRead);
            }
        }
 private static void RefreshTheCache(WebApiCacheItem cacheItem, _IInvocation invocation, HttpRequestMessage request)
 {
     //Question: Should we create the phoenix only on the server that created it the first place?
     if (!Global.Cache.PhoenixFireCage.ContainsKey(cacheItem.Key))
     {
         Global.Cache.PhoenixFireCage[cacheItem.Key] = new WebApiPhoenix(invocation, cacheItem, request);
     }
     Global.Cache.PhoenixFireCage[cacheItem.Key].Reborn();
 }
        private async Task SaveTheResultToCache(HttpActionExecutedContext actionExecutedContext, string storedKey, IAsyncCacheStore cacheStore)
        {
            var responseContent = actionExecutedContext.Response.Content;

            if (responseContent != null)
            {
                var cacheItem = new WebApiCacheItem
                {
                    Key                       = storedKey,
                    Content                   = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false),
                    ResponseHeaders           = responseContent.Headers,
                    ResponseMediaType         = responseContent.Headers.ContentType.MediaType,
                    ResponseCharSet           = responseContent.Headers.ContentType.CharSet,
                    StoreId                   = cacheStore.StoreId,
                    StaleWhileRevalidate      = StaleWhileRevalidate,
                    MaxAge                    = MaxAge,
                    CreatedTime               = DateTime.UtcNow,
                    IgnoreRevalidationRequest = IgnoreRevalidationRequest,
                    StaleIfError              = StaleIfError,
                    AutoRefresh               = AutoRefresh,
                    Path                      = $"{actionExecutedContext.Request.Method} {actionExecutedContext.Request.RequestUri.PathAndQuery}"
                };

                var invocation = GetInvocation(actionExecutedContext.ActionContext);

                if (AutoRefresh)
                {
                    // Create if not there
                    DisposeOldPhoenixAndCreateNew(invocation, cacheItem, actionExecutedContext.Request);
                }

                if (StaleWhileRevalidate > 0) // Only auto refresh when revalidate if this value > 0, creating a Phoenix is optional
                {
                    var context        = GetInvocationContext(actionExecutedContext.ActionContext);
                    var strategy       = (ICacheStrategy)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_strategy];
                    var changeMonitors = strategy.GetChangeMonitors(invocation, context);
                    foreach (var mon in changeMonitors)
                    {
                        mon.CacheMonitorChanged += state => { TryToRefreshCacheWhileRevalidate(storedKey); };
                    }
                }

                actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{cacheItem.Key}-{cacheItem.Checksum}\"");

                var absoluteExpiration = DateTime.UtcNow.AddSeconds(MaxAge + Math.Max(StaleWhileRevalidate, StaleIfError));

                await Task.WhenAll(new[]
                {
                    //Save url base cache key that can map to the real cache key which will be used by EvaluateServerCacheHandler
                    cacheStore.SetAsync(actionExecutedContext.Request.GetUrlBaseCacheKey(), cacheItem.Key, absoluteExpiration),
                    //Save the actual cache
                    cacheStore.SetAsync(cacheItem.Key, cacheItem, absoluteExpiration)
                }).ConfigureAwait(false);
            }
        }
        private void DisposeOldPhoenixAndCreateNew(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            //Question: Should we do it only on the box that created the phoenix the first place?
            Phoenix phoenix;

            if (Global.Cache.PhoenixFireCage.TryGetValue(cacheItem.Key, out phoenix))
            {
                phoenix?.Dispose();
            }

            Global.Cache.PhoenixFireCage[cacheItem.Key] = new WebApiPhoenix(invocation, cacheItem, request);
        }
        /// <summary>
        /// Create the phoenix object which can refresh the cache itself if StaleWhileRevalidate > 0
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="cacheItem"></param>
        /// <param name="request"></param>
        /// <returns></returns>
        private void CreatePhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            if (cacheItem.StaleWhileRevalidate <= 0 || request.Method != HttpMethod.Get)
            {
                return;
            }

            if (Global.Cache.PhoenixFireCage.ContainsKey(cacheItem.Key))
            {
                Global.Cache.PhoenixFireCage[cacheItem.Key].Dispose();
            }
            Global.Cache.PhoenixFireCage[cacheItem.Key] = new WebApiPhoenix(invocation, cacheItem, request);
        }
        public async Task Should_return_new_etag_if_cache_item_found_but_doesnt_match_checksum(string cacheChecksum, HttpStatusCode resultCode)
        {
            // Arrange
            
            var cacheControl = new CacheControlHeaderValue
            {
                MaxStale = true,
                MaxStaleLimit = TimeSpan.FromSeconds(15),
                MinFresh = TimeSpan.FromSeconds(20)
            };

            var oldCacheItem = new WebApiCacheItem
            {
                CreatedTime = DateTime.UtcNow.AddSeconds(-11),
                MaxAge = 10,
                StaleWhileRevalidate = 5,
                IgnoreRevalidationRequest = true,
                ResponseCharSet = "UTF8",
                ResponseMediaType = "text/json",
                Content = new byte[0],
                Key = "fw-0-HASHEDKEY",
                Checksum = cacheChecksum
            };

            var request = new HttpRequestMessage
            {
                Method = new HttpMethod("GET"),
                RequestUri = new Uri("http://localhost")
            };

            request.Headers.Add("If-None-Match", "\"fw-0-HASHEDKEY-OLDCHECKSUM\"");
            var builder = new CacheResponseBuilder();
            var handler = new EtagHeaderHandler(builder);
            await Global.CacheStoreProvider.GetAsyncCacheStore().SetAsync("fw-0-HASHEDKEY", oldCacheItem, DateTimeOffset.Now.AddDays(1)).ConfigureAwait(false);


            // Action
            Global.Cache.PhoenixFireCage["fw-0-HASHEDKEY"] = new WebApiPhoenix(NSubstitute.Substitute.For<_IInvocation>(), new CacheInfo(), oldCacheItem, request);
            var response = await handler.HandleAsync(cacheControl, request, CancellationToken.None).ConfigureAwait(false);

            Assert.AreEqual(resultCode, response.StatusCode);
            if (resultCode == HttpStatusCode.OK)
            {
                Assert.AreEqual($"\"fw-0-HASHEDKEY-{cacheChecksum}\"", response.Headers.ETag.Tag);
            }
            else
            {
                Assert.IsNull(response.Headers.ETag);
            }
        }
Exemple #9
0
        /// <summary>
        /// Create the phoenix object which can refresh the cache itself if StaleWhileRevalidate > 0
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="cacheItem"></param>
        /// <param name="request"></param>
        /// <returns></returns>
        private void CreatePhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            if (cacheItem.StaleWhileRevalidate <= 0 || request.Method != HttpMethod.Get)
            {
                return;
            }

            Phoenix phoenix;

            if (Global.Cache.PhoenixFireCage.TryGetValue(cacheItem.Key, out phoenix))
            {
                phoenix?.Dispose();
            }

            Global.Cache.PhoenixFireCage[cacheItem.Key] = new WebApiPhoenix(invocation, cacheItem, request);
        }
Exemple #10
0
        /// <summary>
        /// Initializes a WebApiPhoenix
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="cacheItem">This should the the WebApiCacheItem instance</param>
        /// <param name="requestMessage"></param>
        public WebApiPhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage requestMessage) : base(invocation, cacheItem)
        {
            _cacheItem            = cacheItem;
            _clonedRequestMessage = new HttpRequestMessage
            {
                RequestUri = requestMessage.RequestUri,
                Method     = requestMessage.Method,
                Version    = requestMessage.Version
            };
            if (!string.IsNullOrWhiteSpace(WebApiExtensions._fwConfig.LoopbackAddress))
            {
                _clonedRequestMessage.RequestUri = new Uri($"{WebApiExtensions._fwConfig.LoopbackAddress}{_clonedRequestMessage.RequestUri.PathAndQuery}");
            }

            _clonedRequestMessage.Content = null;

            foreach (var h in requestMessage.Headers)
            {
                _clonedRequestMessage.Headers.Add(h.Key, h.Value);
            }

            _clonedRequestMessage.Headers.CacheControl = requestMessage.Headers.CacheControl ?? new CacheControlHeaderValue();
        }
        public void Should_not_create_phoenix_for_http_method_not_GET()
        {
            var key = "theCacheKey" + Guid.NewGuid();
            // Arrange
            var objCacheItem = new WebApiCacheItem
            {
                MaxAge = 5,
                StaleWhileRevalidate = 5,
                StoreId = 1000,
                CreatedTime = DateTime.UtcNow.AddSeconds(-5).AddMilliseconds(-1),
                Key = key
            };
            _request.Method = HttpMethod.Post;


            var att = new OutputCacheAttributeWithPublicMethods { MaxAge = 5, CacheStoreId = 1000, StaleWhileRevalidate = 5 };

            // Action
            att.CreatePhoenixPublic(_invocation, objCacheItem, _request);

            // Assert
            Assert.That(!Global.Cache.PhoenixFireCage.ContainsKey(key));
        }
        public void Should_return_304_if_etag_matched()
        {
            // Arrange
            var cacheControl = new CacheControlHeaderValue
            {
                MaxStale = true,
                MaxStaleLimit = TimeSpan.FromSeconds(15),
                MinFresh = TimeSpan.FromSeconds(20)
            };

            var cacheItem = new WebApiCacheItem
            {
                CreatedTime = DateTime.UtcNow.AddSeconds(-11),
                MaxAge = 10,
                StaleWhileRevalidate = 5,
                IgnoreRevalidationRequest = true,
                ResponseCharSet = "UTF8",
                ResponseMediaType = "text/json",
                Content = new byte[0],
                Key = "CacheKey" + Guid.NewGuid()
            };

            Global.Cache.PhoenixFireCage[cacheItem.Key] = new Phoenix(NSubstitute.Substitute.For<_IInvocation>(), new CacheInfo());

            var request = new HttpRequestMessage();
            request.Properties[WebApiExtensions.__webApi_etag_matched] = true;
            var svc = new CacheResponseBuilder { };

            // Action
            var response = svc.GetResponse(cacheControl, cacheItem, request);

            // Assert
            Assert.AreEqual(HttpStatusCode.NotModified, response.StatusCode);
            Assert.AreEqual("Cache freshness lifetime not qualified", response.Headers.GetValues("X-Flatwhite-Warning").First());
            Assert.AreEqual("Response is Stale", response.Headers.GetValues("X-Flatwhite-Warning").Last());
            Assert.AreEqual($"110 - \"Response is Stale\"", response.Headers.GetValues("Warning").Last());
        }
        /// <summary>
        /// Provide a single method to try to build a <see cref="HttpResponseHeaders" /> from <see cref="CacheControlHeaderValue" />  and <see cref="HttpRequestMessage" />
        /// </summary>
        public virtual HttpResponseMessage GetResponse(CacheControlHeaderValue cacheControl, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            if (cacheControl != null &&
                cacheControl.Extensions != null &&
                cacheControl.Extensions.Any(x => x.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh) &&
                request.IsLocal())
            {
                return(null);
            }

            if (cacheControl != null && cacheControl.OnlyIfCached && cacheItem == null)
            {
                var errorResponse = new HttpResponseMessage {
                    StatusCode = HttpStatusCode.GatewayTimeout
                };
                errorResponse.Headers.Add("X-Flatwhite-Message", "no cache available");
                return(errorResponse);
            }

            if (cacheItem == null)
            {
                return(null);
            }

            var ageInSeconds = cacheItem.Age;

            var responseCacheControl = new CacheControlHeaderValue
            {
                MaxAge = TimeSpan.FromSeconds(Math.Max(cacheItem.MaxAge - ageInSeconds, 0)),
            };

            var cacheNotQualified = false;
            //http://stackoverflow.com/questions/1046966/whats-the-difference-between-cache-control-max-age-0-and-no-cache
            bool stale = cacheControl?.MaxAge?.TotalSeconds > 0 && cacheControl.MaxAge.Value.TotalSeconds < ageInSeconds;

            if (cacheItem.IsStale())
            {
                stale = true;
                Global.Logger.Info($"Stale key \"{cacheItem.Key}\", age: \"{ageInSeconds}\", store: \"{cacheItem.StoreId}\", request: {request.RequestUri.PathAndQuery}");

                if (cacheItem.StaleWhileRevalidate > 0 &&
                    cacheControl != null &&
                    cacheControl.MaxStale &&
                    cacheControl.MaxStaleLimit.HasValue &&
                    cacheControl.MaxStaleLimit.Value.TotalSeconds > (ageInSeconds - cacheItem.MaxAge))
                {
                    //  https://tools.ietf.org/html/rfc5861
                    responseCacheControl.Extensions.Add(new NameValueHeaderValue("stale-while-revalidate", cacheItem.StaleWhileRevalidate.ToString()));
                }

                /* It's responsibility is building the response, it should not worry about refresh or anything
                 * if (!Global.Cache.PhoenixFireCage.ContainsKey(cacheItem.Key))
                 * {
                 *  // No phoenix yet, let the OutputCacheFilter created the phoenix and call the builder again
                 *  return null;
                 * }
                 *
                 * if (!cacheItem.AutoRefresh)
                 * {
                 *  Global.Cache.PhoenixFireCage[cacheItem.Key].Reborn();
                 * }
                 */
            }

            var response = request.CreateResponse();

            if (cacheControl?.MinFresh?.TotalSeconds > ageInSeconds)
            {
                response.Headers.Add("X-Flatwhite-Warning", "Cache freshness lifetime not qualified");
                cacheNotQualified = true;
            }

            if ((stale || cacheNotQualified) && !cacheItem.IgnoreRevalidationRequest)
            {
                return(null);
            }

            if (stale)
            {
                request.Properties[WebApiExtensions.__webApi_cache_is_stale] = true;
                response.Headers.Add("X-Flatwhite-Warning", "Response is Stale");
                //https://tools.ietf.org/html/rfc7234#page-31
                response.Headers.Add("Warning", "110 - \"Response is Stale\"");
            }

            response.Headers.Age          = TimeSpan.FromSeconds(ageInSeconds);
            response.Headers.CacheControl = responseCacheControl;

            if (request.Properties.ContainsKey(WebApiExtensions.__webApi_etag_matched))
            {
                response.StatusCode = HttpStatusCode.NotModified;
            }
            else
            {
                response.StatusCode = HttpStatusCode.OK;
                response.Content    = new ByteArrayContent(cacheItem.Content);
                response.Content.Headers.ContentType = new MediaTypeHeaderValue(cacheItem.ResponseMediaType)
                {
                    CharSet = cacheItem.ResponseCharSet
                };
                response.Headers.ETag = new EntityTagHeaderValue($"\"{cacheItem.Key}-{cacheItem.Checksum}\"");
            }
            return(response);
        }
        public void Should_return_null_if_stale()
        {
            // Arrange
            var cacheControl = new CacheControlHeaderValue
            {
                MaxStale = true,
                MaxStaleLimit = TimeSpan.FromSeconds(15)
            };

            var cacheItem = new WebApiCacheItem
            {
                CreatedTime = DateTime.UtcNow.AddSeconds(-11),
                MaxAge = 10,
                StaleWhileRevalidate = 5,
                IgnoreRevalidationRequest = false,
                ResponseCharSet = "UTF8",
                ResponseMediaType = "text/json",
                Content = new byte[0],
                Key = "CacheKey" + Guid.NewGuid()
            };

            Global.Cache.PhoenixFireCage[cacheItem.Key] = new Phoenix(NSubstitute.Substitute.For<_IInvocation>(), new CacheInfo());

            var request = new HttpRequestMessage();
            var svc = new CacheResponseBuilder { };

            // Action
            var response = svc.GetResponse(cacheControl, cacheItem, request);

            // Assert
            Assert.IsNull(response);
        }
Exemple #15
0
        /// <summary>
        /// Store the response to cache store, add CacheControl and Etag to response
        /// </summary>
        /// <param name="actionExecutedContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
        {
            if (ShouldIgnoreCache(actionExecutedContext.Request.Headers.CacheControl, actionExecutedContext.Request))
            {
                return;
            }

            if ((actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) && StaleIfError == 0)
            {
                return; // Early return
            }

            var cacheControl = actionExecutedContext.Request.Headers.CacheControl;

            if (cacheControl?.Extensions != null && cacheControl.Extensions.Any(x => x.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh))
            {
                var entry = cacheControl.Extensions.First(x => x.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh);
                cacheControl.Extensions.Remove(entry);
            }

            ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, actionExecutedContext.Request);

            var storedKey  = (string)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_key];
            var cacheStore = (IAsyncCacheStore)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_store];

            if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode)
            {
                var cacheItem = await cacheStore.GetAsync(storedKey).ConfigureAwait(false) as WebApiCacheItem;

                if (cacheItem != null && StaleIfError > 0)
                {
                    var builder  = (ICacheResponseBuilder)actionExecutedContext.Request.Properties[WebApiExtensions.__webApi_outputcache_response_builder];
                    var response = builder.GetResponse(actionExecutedContext.Request.Headers.CacheControl, cacheItem, actionExecutedContext.Request);

                    if (response != null)
                    {
                        //NOTE: Override error response
                        actionExecutedContext.Response = response;
                    }
                    return;
                }
            }

            var responseContent = actionExecutedContext.Response.Content;

            if (responseContent != null)
            {
                var cacheItem = new WebApiCacheItem
                {
                    Key                       = storedKey,
                    Content                   = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false),
                    ResponseMediaType         = responseContent.Headers.ContentType.MediaType,
                    ResponseCharSet           = responseContent.Headers.ContentType.CharSet,
                    StoreId                   = cacheStore.StoreId,
                    StaleWhileRevalidate      = StaleWhileRevalidate,
                    MaxAge                    = MaxAge,
                    CreatedTime               = DateTime.UtcNow,
                    IgnoreRevalidationRequest = IgnoreRevalidationRequest,
                    StaleIfError              = StaleIfError,
                    AutoRefresh               = AutoRefresh
                };

                var strategy       = (ICacheStrategy)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_strategy];
                var invocation     = GetInvocation(actionExecutedContext.ActionContext);
                var context        = GetInvocationContext(actionExecutedContext.ActionContext);
                var changeMonitors = strategy.GetChangeMonitors(invocation, context);

                CreatePhoenix(invocation, cacheItem, actionExecutedContext.Request);

                foreach (var mon in changeMonitors)
                {
                    mon.CacheMonitorChanged += state =>
                    {
                        RefreshCache(storedKey);
                    };
                }

                actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{cacheItem.Key}-{cacheItem.Checksum}\"");

                var absoluteExpiration = DateTime.UtcNow.AddSeconds(MaxAge + Math.Max(StaleWhileRevalidate, StaleIfError));
                await cacheStore.SetAsync(cacheItem.Key, cacheItem, absoluteExpiration).ConfigureAwait(false);
            }
        }
 /// <summary>
 /// Initializes a WebApiPhoenix
 /// </summary>
 /// <param name="invocation"></param>
 /// <param name="cacheItem">This should the the WebApiCacheItem instance</param>
 /// <param name="originalRequestMessage"></param>
 public WebApiPhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage originalRequestMessage) : base(invocation, cacheItem)
 {
     _cacheItem = cacheItem;
     _originalRequestMessage = originalRequestMessage;
 }
        /// <summary>
        /// Create the phoenix object which can refresh the cache itself if StaleWhileRevalidate > 0
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="cacheItem"></param>
        /// <param name="request"></param>
        /// <returns></returns>
        private void CreatePhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            if (cacheItem.StaleWhileRevalidate <= 0 || request.Method != HttpMethod.Get)
            {
                return;
            }

            Phoenix phoenix;
            if (Global.Cache.PhoenixFireCage.TryGetValue(cacheItem.Key, out phoenix))
            {
                phoenix?.Dispose();
            }

            Global.Cache.PhoenixFireCage[cacheItem.Key] = new WebApiPhoenix(invocation, cacheItem, request);
        }
        /// <summary>
        /// Provide a single method to try to build a <see cref="HttpResponseHeaders" /> from <see cref="CacheControlHeaderValue" />  and <see cref="HttpRequestMessage" />
        /// </summary>
        public virtual HttpResponseMessage GetResponse(CacheControlHeaderValue cacheControl, WebApiCacheItem cacheItem, HttpRequestMessage request)
        {
            if (cacheControl != null && cacheControl.OnlyIfCached && cacheItem == null)
            {
                var errorResponse = new HttpResponseMessage {StatusCode = HttpStatusCode.GatewayTimeout};
                errorResponse.Headers.Add("X-Flatwhite-Message", "no cache available");
                return errorResponse;
            }

            if (cacheItem == null)
            {
                return null;
            }

            var age = cacheItem.Age;

            var responseCacheControl = new CacheControlHeaderValue
            {
                MaxAge = TimeSpan.FromSeconds(Math.Max(cacheItem.MaxAge - age, 0)),
            };

            var cacheNotQualified = false;
            bool stale = cacheControl?.MaxAge?.TotalSeconds > 0 && cacheControl.MaxAge.Value.TotalSeconds < age;

            if (cacheItem.MaxAge < age)
            {
                stale = true;

                if (cacheItem.StaleWhileRevalidate > 0 &&
                    cacheControl != null &&
                    cacheControl.MaxStale &&
                    cacheControl.MaxStaleLimit.HasValue &&
                    cacheControl.MaxStaleLimit.Value.TotalSeconds > (age - cacheItem.MaxAge))
                {
                    //  https://tools.ietf.org/html/rfc5861
                    responseCacheControl.Extensions.Add(new NameValueHeaderValue("stale-while-revalidate", cacheItem.StaleWhileRevalidate.ToString()));
                }

                if (!Global.Cache.PhoenixFireCage.ContainsKey(cacheItem.Key))
                {
                    // No phoenix yet, let the OutputCacheFilter created the phoenix and call the builder again
                    return null;
                }

                if (!cacheItem.AutoRefresh)
                {
                    Global.Cache.PhoenixFireCage[cacheItem.Key].Reborn();
                }
            }

            var response = request.CreateResponse();
            if (cacheControl?.MinFresh?.TotalSeconds > age)
            {
                response.Headers.Add("X-Flatwhite-Warning", "Cache freshness lifetime not qualified");
                cacheNotQualified = true;
            }

            if ((stale || cacheNotQualified) && !cacheItem.IgnoreRevalidationRequest)
            {
                return null;
            }

            if (stale)
            {
                request.Properties[WebApiExtensions.__webApi_cache_is_stale] = true;
                response.Headers.Add("X-Flatwhite-Warning", "Response is Stale");
                //https://tools.ietf.org/html/rfc7234#page-31
                response.Headers.Add("Warning", $"110 - \"Response is Stale\"");
            }

            response.Headers.Age = TimeSpan.FromSeconds(age);
            response.Headers.CacheControl = responseCacheControl;

            if (request.Properties.ContainsKey(WebApiExtensions.__webApi_etag_matched))
            {
                response.StatusCode = HttpStatusCode.NotModified;
            }
            else
            {
                response.StatusCode = HttpStatusCode.OK;
                response.Content = new ByteArrayContent(cacheItem.Content);
                response.Content.Headers.ContentType = new MediaTypeHeaderValue(cacheItem.ResponseMediaType)
                {
                    CharSet = cacheItem.ResponseCharSet
                };
                response.Headers.ETag = new EntityTagHeaderValue($"\"{cacheItem.Key}-{cacheItem.Checksum}\"");
            }
            return response;
        }
 public void CreatePhoenixPublic(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request)
 {
     var methodInfo = typeof(Flatwhite.WebApi.OutputCacheAttribute).GetMethod("CreatePhoenix", BindingFlags.Instance | BindingFlags.NonPublic);
     methodInfo.Invoke(this, new object[] { invocation, cacheItem, request });
 }
        /// <summary>
        /// Store the response to cache store, add CacheControl and Etag to response
        /// </summary>
        /// <param name="actionExecutedContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
        {
            if (ShouldIgnoreCache(actionExecutedContext.Request.Headers.CacheControl, actionExecutedContext.Request))
            {
                return;
            }

            if ((actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) && StaleIfError == 0)
            {
                return; // Early return
            }

            var cacheControl = actionExecutedContext.Request.Headers.CacheControl;
            if (cacheControl?.Extensions != null && cacheControl.Extensions.Any(x => x.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh))
            {
                var entry = cacheControl.Extensions.First(x => x.Name == WebApiExtensions.__cacheControl_flatwhite_force_refresh);
                cacheControl.Extensions.Remove(entry);
            }

            ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, actionExecutedContext.Request);

            var storedKey = (string)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_key];
            var cacheStore = (IAsyncCacheStore)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_store];

            if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode)
            {
                var cacheItem = await cacheStore.GetAsync(storedKey).ConfigureAwait(false) as WebApiCacheItem;
                if (cacheItem != null && StaleIfError > 0)
                {
                    var builder = (ICacheResponseBuilder)actionExecutedContext.Request.Properties[WebApiExtensions.__webApi_outputcache_response_builder];
                    var response = builder.GetResponse(actionExecutedContext.Request.Headers.CacheControl, cacheItem, actionExecutedContext.Request);

                    if (response != null)
                    {
                        //NOTE: Override error response
                        actionExecutedContext.Response = response;
                    }
                    return;
                }
            }

            var responseContent = actionExecutedContext.Response.Content;

            if (responseContent != null)
            {
                var cacheItem = new WebApiCacheItem
                {
                    Key = storedKey,
                    Content = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false),
                    ResponseMediaType = responseContent.Headers.ContentType.MediaType,
                    ResponseCharSet = responseContent.Headers.ContentType.CharSet,
                    StoreId = cacheStore.StoreId,
                    StaleWhileRevalidate = StaleWhileRevalidate,
                    MaxAge = MaxAge,
                    CreatedTime = DateTime.UtcNow,
                    IgnoreRevalidationRequest = IgnoreRevalidationRequest,
                    StaleIfError = StaleIfError,
                    AutoRefresh = AutoRefresh
                };
                
                var strategy = (ICacheStrategy)actionExecutedContext.Request.Properties[Global.__flatwhite_outputcache_strategy];
                var invocation = GetInvocation(actionExecutedContext.ActionContext);
                var context = GetInvocationContext(actionExecutedContext.ActionContext);
                var changeMonitors = strategy.GetChangeMonitors(invocation, context);

                CreatePhoenix(invocation, cacheItem, actionExecutedContext.Request);
                
                foreach (var mon in changeMonitors)
                {
                    mon.CacheMonitorChanged += state =>
                    {
                        RefreshCache(storedKey);
                    };
                }

                actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{cacheItem.Key}-{cacheItem.Checksum}\"");

                var absoluteExpiration =  DateTime.UtcNow.AddSeconds(MaxAge + Math.Max(StaleWhileRevalidate, StaleIfError));
                await cacheStore.SetAsync(cacheItem.Key, cacheItem, absoluteExpiration).ConfigureAwait(false);
            }
        }
        public async Task Should_create_phoenix_and_try_refresh_cache_when_cache_item_is_stale()
        {
            // Arrange
            var store = Substitute.For<IAsyncCacheStore>();
            store.StoreId.Returns(1000);
            var objCacheItem = new WebApiCacheItem
            {
                MaxAge = 5,
                StaleWhileRevalidate = 5,
                StoreId = 1000,
                CreatedTime = DateTime.UtcNow.AddSeconds(-5).AddMilliseconds(-1),
            };

            store.GetAsync(Arg.Any<string>()).Returns(c =>
            {
                objCacheItem.Key = c.Arg<string>();
                return Task.FromResult((object)objCacheItem);
            });

            Global.CacheStoreProvider.RegisterAsyncStore(store);
            var att = new Flatwhite.WebApi.OutputCacheAttribute { MaxAge = 5, CacheStoreId = 1000, StaleWhileRevalidate = 5 };

            // Action
            await att.OnActionExecutingAsync(_actionContext, CancellationToken.None);

            // Assert
            Assert.IsTrue(Global.Cache.PhoenixFireCage.ContainsKey(objCacheItem.Key));
        }
        public void should_execute_the_controller_method_and_return_CacheItem(string actionMethodName, int contentLength)
        {
            // Arrange
            var currentCacheItem = new WebApiCacheItem();
            var invocation = Substitute.For<_IInvocation>();
            invocation.Arguments.Returns(new object[0]);
            invocation.Method.Returns(controllerType.GetMethod(actionMethodName, BindingFlags.Instance | BindingFlags.Public));

            var phoenix = new WebApiPhoenix(invocation, CacheInfo, currentCacheItem, new HttpRequestMessage(), new JsonMediaTypeFormatter());

            // Action
            MethodInfo dynMethod = typeof(WebApiPhoenix).GetMethod("InvokeAndGetBareResult", BindingFlags.NonPublic | BindingFlags.Instance);
            var result = dynMethod.Invoke(phoenix, new object[] { _controllerIntance });

            dynMethod = typeof(WebApiPhoenix).GetMethod("GetCacheItem", BindingFlags.NonPublic | BindingFlags.Instance);
            var cacheItem = (WebApiCacheItem)dynMethod.Invoke(phoenix, new[] { result });

            // Assert
            if (result == null)
            {
                Assert.IsTrue(actionMethodName == "Void" || actionMethodName == "VoidAsync");
            }
            else
            {
                Assert.AreEqual(contentLength, cacheItem.Content.Length);
            }
        }
        public void Should_return_null_if_cache_not_mature_as_min_fresh_request()
        {
            // Arrange
            var cacheControl = new CacheControlHeaderValue
            {
                MinFresh = TimeSpan.FromSeconds(100)
            };

            var cacheItem = new WebApiCacheItem
            {
                CreatedTime = DateTime.UtcNow.AddSeconds(-20),
                MaxAge = 1000,
                StaleWhileRevalidate = 5,
                IgnoreRevalidationRequest = false
            };

            var request = new HttpRequestMessage();
            var svc = new CacheResponseBuilder { };

            // Action
            var response = svc.GetResponse(cacheControl, cacheItem, request);

            // Assert
            Assert.IsNull(response);
        }
 public WebApiPhoenixWithPublicMethods(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage originalRequestMessage)
     : base(invocation, cacheItem, originalRequestMessage)
 {
 }
        /// <summary>
        /// Create the phoenix object which can refresh the cache itself if StaleWhileRevalidate > 0
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="cacheItem"></param>
        /// <param name="request"></param>
        /// <param name="mediaTypeFormatter">The formater that was used to create the reasponse at the first invocation</param>
        /// <returns></returns>
        private void CreatePhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage request, MediaTypeFormatter mediaTypeFormatter)
        {
            var cacheInfo = new CacheInfo
            {
                CacheKey = cacheItem.Key,
                CacheStoreId = cacheItem.StoreId,
                CacheDuration = MaxAge,
                StaleWhileRevalidate = StaleWhileRevalidate,
                AutoRefresh = AutoRefresh
            };

            var phoenix = new WebApiPhoenix(invocation, cacheInfo, cacheItem, request, mediaTypeFormatter);
            if (Global.Cache.PhoenixFireCage.ContainsKey(cacheItem.Key))
            {
                Global.Cache.PhoenixFireCage[cacheItem.Key].Dispose();
            }
            Global.Cache.PhoenixFireCage[cacheItem.Key] = phoenix;
        }
        public async Task Phoenix_action_should_display_all_phoenix_in_cache()
        {
            Global.Cache.PhoenixFireCage.Add("item1", Substitute.For<Phoenix>(Substitute.For<_IInvocation>(), new CacheItem { Data = "data", Key = "item1" }));
            Global.Cache.PhoenixFireCage.Add("item2", Substitute.For<Phoenix>(Substitute.For<_IInvocation>(), new CacheItem { Data = new MediaTypeHeaderValue("text/json"), Key = "item2" }));
            Global.Cache.PhoenixFireCage.Add("item5", Substitute.For<Phoenix>(Substitute.For<_IInvocation>(), new CacheItem()));
            Global.Cache.PhoenixFireCage.Add("item6", Substitute.For<Phoenix>(Substitute.For<_IInvocation>(), new CacheItem()));

            var syncStore = Substitute.For<ICacheStore>();
            syncStore.Get(Arg.Any<string>()).Returns(c => new CacheItem
            {
                Key = c.Arg<string>(),
                Data = "data"
            });

            var asyncStore = Substitute.For<IAsyncCacheStore>();
            asyncStore.GetAsync(Arg.Any<string>()).Returns(c =>
            {
                object obj = new WebApiCacheItem
                {
                    Key = c.Arg<string>(),
                    Content = new byte[0]
                };
                return Task.FromResult(obj);
            });

            var provider = Substitute.For<ICacheStoreProvider>();
            provider.GetCacheStore(Arg.Any<int>()).Returns(syncStore);
            provider.GetAsyncCacheStore(Arg.Any<int>()).Returns(asyncStore);
            var controller = new FlatwhiteStatusController(provider);

            // Action
            var result = await controller.Phoenix();
            var jsonResult = (JsonResult<List<FlatwhiteStatusController.CacheItemStatus>>)result;

            // Assert
            Assert.AreEqual(4, jsonResult.Content.Count);
        }
Exemple #27
0
 /// <summary>
 /// Initializes a WebApiPhoenix
 /// </summary>
 /// <param name="invocation"></param>
 /// <param name="cacheItem">This should the the WebApiCacheItem instance</param>
 /// <param name="originalRequestMessage"></param>
 public WebApiPhoenix(_IInvocation invocation, WebApiCacheItem cacheItem, HttpRequestMessage originalRequestMessage) : base(invocation, cacheItem)
 {
     _cacheItem = cacheItem;
     _originalRequestMessage = originalRequestMessage;
 }