I'm using the OutputCache on my webuser control (.ascx)
<%# OutputCache Duration="1000" VaryByParam="none" %>
I would like to retain the cache for next 1000 seconds, but when a specific page on my site is loaded, I would like to remove/flush/refresh the cache. Like, I want to clear the cache when MyPage.aspx is loaded. Can i flush the cache programmetically?
Its only one page being cache so there are no paramatrized versions to flush cache with.
Thanks for your help in advance.
You can use the VaryByCustom parameter for this.
In your user control you would have the following:
<%# OutputCache Duration="1000" VaryByParam="None" VaryByCustom="MyKey" %>
Then you would override the GetVaryByCustomString method in your Global.asax like so:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "MyKey")
{
object o = context.Current.Application["MyGuid"];
if (o == null)
{
o = Guid.NewGuid();
context.Current.Application["MyGuid"] = o;
}
return o.ToString();
}
return base.GetVaryByCustomString(context, arg);
}
Finally in MyPage.aspx you would do this:
Application["MyGuid"] = Guid.NewGuid();
How does this work?
Whenever your control is cached, it is associated with a string (the string returned from the GetVaryByCustomString method when your control's VaryByCustom key is passed into it).
Whenever the control is subsequently used, GetVaryByCustomString is called again. If the returned string matches a cached version of the control, then the cached version is used.
In our case, "MyKey" is passed into GetVaryByCustomString and it returns whatever is stored in Application["MyGuid"].
Whenever MyPage.aspx is called, it changes Application["MyGuid"] to a new random value.
When your control is next used the GetVaryByCustomString method will return the new value, and because there is no cached version of the control associated with that value, the control will be regenerated. (The control will then be cached and associated with the new value, to persist until the next call to MyPage.aspx etc)
There's an overview here.
You can use HttpResponse.RemoveOutputCacheItem or HttpResponse.AddCacheItemDependency to invalidate output cache entries.
It's a bit of a sledgehammer to crack a nut sort of approach but it was the simplest and most effective way to completely clear/flush the cache of an application that I found find.
Simply call:
HttpRuntime.UnloadAppDomain();
This has the same impact as recycling the app pool. Not suitable in every situation but it will definitely get the job done.
By taking inspiration on other post, the following snippet removes successfully every page cached by OutputCache in asp.net by using reflection:
public static void ClearOutputCache()
{
var runtimeType = typeof(HttpRuntime);
var ci = runtimeType.GetProperty(
"CacheInternal",
BindingFlags.NonPublic | BindingFlags.Static);
var cache = ci.GetValue(ci, new object[0]);
var cachesInfo = cache.GetType().GetField(
"_caches",
BindingFlags.NonPublic | BindingFlags.Instance);
var cacheEntries = cachesInfo.GetValue(cache);
var outputCacheEntries = new List<object>();
foreach (Object singleCache in cacheEntries as Array)
{
var singleCacheInfo =
singleCache.GetType().GetField("_entries",
BindingFlags.NonPublic | BindingFlags.Instance);
var entries = singleCacheInfo.GetValue(singleCache);
foreach (DictionaryEntry cacheEntry in entries as Hashtable)
{
var cacheEntryInfo = cacheEntry.Value.GetType().GetField("_value",
BindingFlags.NonPublic | BindingFlags.Instance);
var value = cacheEntryInfo.GetValue(cacheEntry.Value);
if (value.GetType().Name == "CachedRawResponse")
{
var key = (string)cacheEntry.Value.GetType().BaseType.GetField("_key", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(cacheEntry.Value);
key = key.Substring(key.IndexOf("/"));
outputCacheEntries.Add(key);
}
}
}
foreach (string key in outputCacheEntries)
{
HttpResponse.RemoveOutputCacheItem(key);
}
}
Edit: If you have enabled kernel caching on II6+ then you will need to go with Luke's advice and use a VaryByCustom header, as clearing ASP.NET cache will not affect kernel cache.
OutputCache is stored in the ASP.NET Cache, so you can just call Cache.Remove:
List<string> keys = new List<string>();
foreach(string key in HttpRuntime.Cache)
{
keys.Add(key);
}
foreach(string key in keys)
{
Cache.Remove(key);
}
This will, however, remove ALL cache entries, including custom entries added by your code.
Sinсe this answer no longer works, and this answer only clears custom cache items (not the pages/MVC-actions stored in cache), I digged through ASP.NET reference source codes and came up with this:
public static void ClearAllCache()
{
var runtimeType = typeof(System.Web.Caching.Cache);
var ci = runtimeType.GetProperty(
"InternalCache",
BindingFlags.Instance | BindingFlags.NonPublic);
var cache = ci.GetValue(HttpRuntime.Cache) as System.Web.Caching.CacheStoreProvider;
enumerator = cache.GetEnumerator();
while (enumerator.MoveNext())
{
keys.Add(enumerator.Key.ToString());
}
foreach (string key in keys)
{
cache.Remove(key);
}
}
More info in my blog post here
you can add HttpResponse.RemoveOutputCacheItem("/YourCachePageName.aspx"); line of code in the page load event of the page, loading of which you expect the cached page's cache to go off.
Related
I have partially implemented Globalization/Localization in my project. The project requires a database to be used for resource strings and I found an excellent NuGet package called WestWind.Globalization that does exactly what I needed.
This NuGet package allows you to display resource strings using several different methods. It provides an option to generate a strongly typed class that contains all of your resource strings so you can use it like:
#Html.Encode( Resources.lblResourceName )
or
object Value = this.GetLocalResourceObject("ResourceName");
or
object GlobalValue = this.GetGlobalResourceObject("Resources","ResourceKey");
and even:
dbRes.T(resourceName, resourceSet, culture)
I didn't want to specify the culture manually, so I opted for this method:
<p class="pageprompt">#AccountRequestAccount.pagePrompt</p>
For me, Westwind.Globalization is magical. It resolved a huge issue for me, but I ran into a snag that I wasn't sure how to overcome. That was, how to set the Culture/CultureUI so that the package would automatically use a specified language resource.
I created a PartialView that contains a dropdown list of languages. It is contained in the ~/Views/Shared/ folder and gets included in _Layout.cshtml. I coded the GET and POST Controller Actions which work as intended, except that I was unable to persist the Culture/CultureUI settings. I suspect that it was due to a redirect immediately following language selection (explained below)
So, I found an SO question that had an answer that seemed viable. I integrated that answer into my project. The relevant code is:
RouteConfig.cs:
routes.MapRoute("DefaultLocalized",
"{language}-{culture}/{controller}/{action}/{id}",
new
{
controller = "Home",
action = "Index",
id = "",
language = "en",
culture = "US"
});
~/Helpers/InternationalizationAttribute.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace GPS_Web_App.Helpers
{
public class InternationalizationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string language =
(string)filterContext.RouteData.Values["language"] ?? "en";
string culture =
(string)filterContext.RouteData.Values["culture"] ?? "US";
Thread.CurrentThread.CurrentCulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
}
}
}
In my Controllers:
[Authorize]
[Internationalization]
public class AccountController : Controller
{
...
}
So far so good. This works in that I am able to go to a URL of http://example.com/en-mx/Account/Login/ and see the page being localized by Westwind.Globalization and the resource strings I've created.
The problems I have with this are:
If the user is anonymous their language preference should be controlled by cookie (if it exists) otherwise default to en-US.
If the user is authenticated their language preference should be controlled by the Language field in their profile settings. (Simple Membership using ASP.NET Identity 2.0).
There is a language selection dropdown in a global header. The user should be able to choose their language preference from the dropdown and if they do, the setting gets written to cookie (for both anonymous and authenticated users) and if the user is authenticated their Language setting in the user profile gets updated.
Not the end of the world, but it would be highly preferable that the language not be included in the URL. Some might ask, well why did I install #jao's solution? Let me explain that.
All of the code was in place for the dropdown to allow a user to make a language selection. The logic for #1, #2, and #3 above were working correctly, but wouldn't take effect and trigger Westwind.Globalization's DbResourceProvider to pass the selected language resource strings.
What I discovered through debugging was that my settings were not persisting in:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
System.Threading.Thread.CurrentThread.CurrentUICulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
Through responses provided by my question here on SO I learned that those settings would not persist/take effect if a redirect was made prior to the original View rendering. Yet a redirect back to the original View seemed sensible since the language was being changed and needed to be rendered again. I think #jao's solution overcomes the redirect problem, but it forces Globalization/Localization to be specified by the URL? Somewhat of a catch-22...
I have asked #jao to review this question and provide any hints on this. I think my question is best summed up as this:
How can I use the user's cookie/profile settings to set the Culture/CultureUI once and for all so that Westwind.Globalization can read Globalization/Localization instead of relying on the Culture being passed in the URL?
I am posting this answer as an alternate, custom way of doing localization with ASP.NET MVC5 with asynchronous controller. Perhaps you may find some gotchas in my solution especially when it comes to routing and setting cookies.
This is sort of a short tutorial I scribbled down for my heterogeneous/custom approach. So I preferred SO over WordPress. :)
Sorry for not giving the precise and discrete answer to your problem. Hopefully it will help you in some other way, and other folks as well; who are looking to do the same sort of setup.
In his blog post, Nadeem Afana described a strategy of creating a separate project Resource in the solution to implement internationalization using static resource files. In the blog sequel, he detailed on extending the same project to handle resources via Databases and XML-driven approaches. For the former one, he used ADO.NET, decoupled from Entity Framework.
We needed to implement both static and dynamic resources within the MVC project, respecting the concepts of MVC conventions.
First lets add a Resources folder in project root, with the desired language variants: ~/Resources/Resources.resx (the default resource file corresponds to en-US culture), ~/Resources/Resources.fi.resx and ~/Resources/Resources.nl.resx. Mark the resources as public, so to make them available in Views.
In ~/Views/Web.config, add the resources namespace under <namespace> element: <add namespace="YourMainNamespace.Reousrces" />. Under controllers, create a base controller class:
Here comes the cookies
namespace YourNamespace.Controllers
{
// Don't forget to inherit other controllers with this
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = null;
// Attempt to read the culture cookie from Request
HttpCookie cultureCookie = Request.Cookies["_culture"];
if (cultureCookie != null)
cultureName = cultureCookie.Value;
else
cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
Request.UserLanguages[0] : // obtain it from HTTP header AcceptLanguages
null;
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}
}
Next, register a global filter to in ~/Global.asax.cs to ensure that every action should use the correct culture before executing:
Here comes the cookies again!
public class SetCultureActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var response = filterContext.RequestContext.HttpContext.Response;
var culture = filterContext.RouteData.Values["culture"].ToString();
// Validate input
culture = CultureHelper.GetImplementedCulture(culture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
// Save culture in a cookie
HttpCookie cookie = filterContext.RequestContext.HttpContext.Request.Cookies["_culture"];
if (cookie != null)
cookie.Value = culture; // update cookie value
else
{
cookie = new HttpCookie("_culture");
cookie.Value = culture;
cookie.Expires = DateTime.Now.AddYears(1);
}
response.Cookies.Add(cookie);
}
}
And add GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute()); in MyApplication.Application_Start() method.
In ~/App_Start/RoutesConfig.cs, change the default route to:
routes.MapRoute(
name: "Default",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "en-US", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
At this point, we would be able to use resources in view. For instance; #Resources.Headline.
Next, we will create a custom attribute called Translatable for model properties.
class TranslatableAttribute : Attribute
{ }
This is enough. But if you want to be able to specify scope, you can use this class to implement it.
Now add a model called Resource with three properties and a helper method:
public class Resource
{
[Key, Column(Order = 0)]
public string Culture { get; set; }
[Key, Column(Order = 1)]
public string Name { get; set; }
public string Value { get; set; }
#region Helpers
// Probably using reflection not the best approach.
public static string GetPropertyValue<T>(string id, string propertyName) where T : class
{
return GetPropertyValue<T>(id, propertyName, Thread.CurrentThread.CurrentUICulture.Name);
}
public static string GetPropertyValue<T>(string id, string propertyName, string culture) where T : class
{
Type entityType = typeof(T);
string[] segments = propertyName.Split('.');
if (segments.Length > 1)
{
entityType = Type.GetType("YourNameSpace.Models." + segments[0]);
propertyName = segments[1];
}
if (entityType == null)
return "?<invalid type>";
var propertyInfo = entityType.GetProperty(propertyName);
var translateableAttribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true)
.FirstOrDefault();
/*var requiredAttribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true)
.FirstOrDefault();*/
if (translateableAttribute == null)
return "?<this field has no translatable attribute>";
var dbCtx = new YourNamespaceDbContext();
var className = entityType.Name;
Resource resource = dbCtx.Resources.Where(r =>
(r.Culture == culture) &&
r.Name == className + id + propertyName).FirstOrDefault();
if (resource != null)
return resource.Value;
//return requiredAttribute == null ? string.Empty : "?<translation not found>";
return string.Empty;
}
#endregion
}
This helper method will help you retrieve the translated content. For instance in view, you can say:
var name = Resource.GetPropertyValue<Product>(item.Id.ToString(), "Name");
Note that, at any point, the data in the translatable field column is unreliable; it will always hold the last updated value. On creating the record, we will mirror all the translatable properties' values in Resource model for all supported cultures.
We are using asynchronous controllers, so for insertion, modification and deletion we will be overriding SaveChangesAsync() in our DbContext class:
public override Task<int> SaveChangesAsync()
{
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectDeletedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
.ToList();
List<ObjectStateEntry> objectCreateOrModifiedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified)
.ToList();
// First handle the delition case,
// before making changes to entry state
bool changed = UpdateResources(objectDeletedStateEntryList);
// Now save the changes
int result = base.SaveChangesAsync().Result;
// Finally handle the remaining cases
changed |= UpdateResources(objectCreateOrModifiedStateEntryList);
if (changed)
return base.SaveChangesAsync();
return Task.FromResult<int>(result);
}
private bool UpdateResources(List<ObjectStateEntry> objectStateEntryList)
{
bool changed = false;
foreach (ObjectStateEntry entry in objectStateEntryList)
{
var typeName = entry.EntitySet.ElementType.Name;
if (entry.IsRelationship || typeName == "Resource")
return false;
var type = Type.GetType("YourNamespace.Models." + typeName);
if (type == null) // When seeds run (db created for the first-time), sometimes types might not be create
return false;
if (entry.State == EntityState.Deleted)
{
changed |= DeleteResources(type, typeName, entry);
continue;
}
foreach (var propertyInfo in type.GetProperties())
{
var attribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true).FirstOrDefault();
if (attribute == null)
continue;
CurrentValueRecord current = entry.CurrentValues;
object idField = current.GetValue(current.GetOrdinal("Id"));
if (idField == null)
continue;
var id = idField.ToString();
var propertyName = propertyInfo.Name;
string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
var name = typeName + id + propertyName;
Resource existingResource = this.Resources.Find(Thread.CurrentThread.CurrentUICulture.Name, name);
if (existingResource == null)
{
foreach (var culture in CultureHelper.Cultures)
{
this.Resources.Add(new Resource
{
Culture = culture,
Name = name,
Value = newValue
});
changed |= true;
}
}
else
{
existingResource.Value = newValue;
changed |= true;
}
}
}
return changed;
}
private bool DeleteResources(Type type, string typeName, ObjectStateEntry entry)
{
bool changed = false;
var firstKey = entry.EntityKey.EntityKeyValues.Where(k => k.Key.Equals("Id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (firstKey == null)
return false;
var id = firstKey.Value.ToString();
foreach (var propertyInfo in type.GetProperties())
{
var name = typeName + id + propertyInfo.Name;
foreach (var culture in CultureHelper.Cultures)
{
Resource existingResource = this.Resources.Find(culture, name);
if (existingResource == null)
continue;
this.Resources.Remove(existingResource);
changed |= true;
}
}
return changed;
}
This will take care of update and delete.
I have a simple site that I want to enable output caching for, but it uses ASP.NET Web Pages (http://asp.net/web-pages, not Web Forms or MVC). Any guidance?
You can put this at the top of any page you want to cache on the server:
Response.Cache.SetCacheability(HttpCacheability.Server);
Response.Cache.SetExpires(DateTime.Now.AddSeconds(60));
Response.Cache.SetValidUntilExpires(true);
Response.Cache.VaryByParams.IgnoreParams = true;
Of, if you want to cache all pages in a folder, put the code in a _PageStart file.
More information on MSDN: http://msdn.microsoft.com/en-us/library/system.web.httpresponse.cache.aspx
The following code does everything you need, and has several overloads to fine tune your output cache for a given page:
#{
var seconds = 600; //10min
Response.OutputCache(seconds);
}
Behind the scenes, this is an extension method contained in System.Web.WebPages.dll assembly that do this:
internal static void OutputCache(HttpContextBase httpContext, HttpCachePolicyBase cache, int numberOfSeconds, bool sliding, IEnumerable<string> varyByParams, IEnumerable<string> varyByHeaders, IEnumerable<string> varyByContentEncodings, HttpCacheability cacheability)
{
cache.SetCacheability(cacheability);
cache.SetExpires(httpContext.Timestamp.AddSeconds((double) numberOfSeconds));
cache.SetMaxAge(new TimeSpan(0, 0, numberOfSeconds));
cache.SetValidUntilExpires(true);
cache.SetLastModified(httpContext.Timestamp);
cache.SetSlidingExpiration(sliding);
if (varyByParams != null)
{
foreach (string index in varyByParams)
cache.VaryByParams[index] = true;
}
if (varyByHeaders != null)
{
foreach (string index in varyByHeaders)
cache.VaryByHeaders[index] = true;
}
if (varyByContentEncodings == null)
return;
foreach (string index in varyByContentEncodings)
cache.VaryByContentEncodings[index] = true;
}
I'm trying to do something which should be very simple...I have a site with a dropdown from which the user selects a group. Thereafter, the user navigates through the site using querystring arguments from menus. So I want the caching to be dependent on the querystring - this seems to work. I also want the cache to be dependent on the group that they selected.
But when the querystring is empty, neither cache element seems to work - the page is just whatever the version was for the last selected group. My cache directive looks like this:
<%# OutputCache Duration="300" VaryByCustom="currentAtomId" VaryByParam="documentId;folderId;sectionId;renderMode;typeId" %>
My varyByCustom code looks like this:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
switch (custom)
{
case "currentAtomId":
var currentAtomId = SecurityManifold.Create().CurrentAtomId;
var returnString = currentAtomId == null ? Guid.NewGuid().ToString() : currentAtomId.ToString();
return returnString;
default:
throw new ArgumentException(string.Format("Argument '{0}' is not a valid cache argument.", custom));
}
}
The call to CurrentAtomId boils down to this:
public static int? GetCurrentAtomIdFromContext(HttpContext context)
{
int entityId;
if (context.Session == null)
{
throw new InvalidOperationException("Session is null");
}
var sessionEntityId = context.Session["CurrentEntityId"];
if (sessionEntityId == null || string.IsNullOrEmpty(sessionEntityId.ToString()))
{
return null;
}
if (!int.TryParse(sessionEntityId.ToString(), out entityId))
{
return null;
}
return entityId;
}
Finally, the code which specifies the CurrentEntityId is this:
var selectedEntityId = this.lstSecurityEntities.SelectedValue;
if (string.IsNullOrEmpty(selectedEntityId))
{
return;
}
Session["CurrentEntityId"] = selectedEntityId;
var possibleQueryString = Request.QueryString.ToString();
if (!string.IsNullOrEmpty(possibleQueryString))
{
possibleQueryString = "?" + possibleQueryString;
}
Response.Redirect("default.aspx" + possibleQueryString);
I'm baffled. Any thoughts would be appreciated.
I eventually determined the problem - when output caching is placed at a PAGE level (as opposed to a control level), the session is not available, and throws an exception. Because this exception is occurring in Global ABOVE the global error handler, it fails silently. I eventually figured this out by wrapping a try-catch block around the cache key generation code in VaryByCustomString and Response.Write-ing it out.
What a beatdown...at any rate, the solution is to implement caching at the control level, which unfortunately is a lot more work because the pieces of the page work together...but it's better than no caching. I hope this helps save somebody else some time.
Bottom Line: for varyByCustomString in global.asax - SESSION IS NOT AVAILABLE WHEN CACHING AT THE PAGE LEVEL.
I have some problems with this code:
private void BoxVisibility(bool email, bool link, Control linkButton)
{
mainEmail.Visible = email;
foreach (Control c in PlaceHolder1.Controls)
{
c.Visible = false;
}
if (linkButton != null)
{
PlaceHolder1.Visible = true;
linkButton.Visible = link;
}
}
when I send to method Control all is ok, but when at first I put Control in Session['temp'] = Control, and then invoke the method like BoxVisibility(false, true, (Control) this.Session['temp']) in this case Control linkButton.Visible = link; doesn't take true, He's still remain false.
The "control" is a reference to a particular instance for this particular page.
The Page is recreated on every request, so a reference to an old instance of your control is no longer valid. So don't store controls in anything (Session, ViewState, static fields) that lives longer than the single request. Local (instance) fields of your page are safe.
If you cache pages in a HttpHandler with
_context.Response.Cache.SetCacheability(HttpCacheability.Public);
_context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(180));
is it possible to clear a certain page from the cache?
is it possible to Clear a certain page
from the Cache?
Yes:
HttpResponse.RemoveOutputCacheItem("/pages/default.aspx");
You can also use Cache dependencies to remove pages from the Cache:
this.Response.AddCacheItemDependency("key");
After making that call, if you modify Cache["key"], it will cause the page to be removed from the Cache.
In case it might help, I cover caching in detail in my book: Ultra-Fast ASP.NET.
more simple one...
public static void ClearCache()
{
Cache cache = HttpRuntime.Cache;
IDictionaryEnumerator dictionaryEnumerators = cache.GetEnumerator();
foreach (string key in (IEnumerable<string>) dictionaryEnumerators.Key)
{
cache.Remove(key);
}
}
The following code will remove all keys from the cache:
public void ClearApplicationCache(){
List<string> keys = new List<string>();
// retrieve application Cache enumerator
IDictionaryEnumerator enumerator = Cache.GetEnumerator();
// copy all keys that currently exist in Cache
while (enumerator.MoveNext()){
keys.Add(enumerator.Key.ToString());
}
// delete every key from cache
for (int i = 0; i < keys.Count; i++) {
Cache.Remove(keys[i]);
}
}
Modifying the second loop to check the value of the key before removing it should be trivial.
Hope this helps.