Creating Canonical URLs including an id and title slug - asp.net-mvc-3

I want to replicate what StackOverflow does with its URLs.
For example:
Hidden Features of C#? - (Hidden Features of C#?)
or
Hidden Features of C#? - (Hidden Features of C#?)
Will Take you to the same page but when they return to the browser the first one is always returned.
How do you implement the change so the larger URL is returned?

The way that I've handled this before is to have two routes, registered in this order
routes.MapRoute(
null,
"questions/{id}/{title}",
new { controller = "Questions", action = "Index" },
new { id = #"\d+", title = #"[\w\-]*" });
routes.MapRoute(
null,
"questions/{id}",
new { controller = "Questions", action = "Index" },
new { id = #"\d+" });
now in the controller action,
public class QuestionsController
{
private readonly IQuestionRepository _questionRepo;
public QuestionsController(IQuestionRepository questionRepo)
{
_questionRepo = questionRepo;
}
public ActionResult Index(int id, string title)
{
var question = _questionRepo.Get(id);
if (string.IsNullOrWhiteSpace(title) || title != question.Title.ToSlug())
{
return RedirectToAction("Index", new { id, title = question.Title.ToSlug() }).AsMovedPermanently();
}
return View(question);
}
}
We'll permanently redirect to the URL that contains the title slug (lowercase title with hyphens as separators) if we only have the id. We also make sure that the title passed is the correct one by checking it against the slugged version of the question title, thereby creating a canonical URL for the question that contains both the id and the correct title slug.
A couple of the helpers used
public static class PermanentRedirectionExtensions
{
public static PermanentRedirectToRouteResult AsMovedPermanently
(this RedirectToRouteResult redirection)
{
return new PermanentRedirectToRouteResult(redirection);
}
}
public class PermanentRedirectToRouteResult : ActionResult
{
public RedirectToRouteResult Redirection { get; private set; }
public PermanentRedirectToRouteResult(RedirectToRouteResult redirection)
{
this.Redirection = redirection;
}
public override void ExecuteResult(ControllerContext context)
{
// After setting up a normal redirection, switch it to a 301
Redirection.ExecuteResult(context);
context.HttpContext.Response.StatusCode = 301;
context.HttpContext.Response.Status = "301 Moved Permanently";
}
}
public static class StringExtensions
{
private static readonly Encoding Encoding = Encoding.GetEncoding("Cyrillic");
public static string RemoveAccent(this string value)
{
byte[] bytes = Encoding.GetBytes(value);
return Encoding.ASCII.GetString(bytes);
}
public static string ToSlug(this string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var str = value.RemoveAccent().ToLowerInvariant();
str = Regex.Replace(str, #"[^a-z0-9\s-]", "");
str = Regex.Replace(str, #"\s+", " ").Trim();
str = str.Substring(0, str.Length <= 200 ? str.Length : 200).Trim();
str = Regex.Replace(str, #"\s", "-");
str = Regex.Replace(str, #"-+", "-");
return str;
}
}

Related

Accessing elements of types with runtime indexers using expression trees

I want to validate rule against input array data with runtime indexer not with some fixed zero index value.
It works if i insert data one by one in session.Insert()
It does't work if i use sesion.InsertAll() with array data
I tried providing Expression.Constant value to indexer but no action triggers
class Program
{
static void Main(string[] args)
{
RuleTestWithSingleInsertData();
// RuleTestWithInsertDataAll();
Console.ReadKey();
}
public static void RuleTestWithSingleInsertData()
{
try
{
CustomRuleRepository repository = new CustomRuleRepository();
List<RuleEngineEntity> rules = new List<RuleEngineEntity>();
rules.Add(new RuleEngineEntity { FieldName = "Age", Name = "CustomerCheck", Value = 25 });
repository.LoadRuleForTest1(rules.FirstOrDefault());
//Compile rules
var factory = repository.Compile();
//Create a working session
var session = factory.CreateSession();
RuleEngineRequestModel ruleEngineRequestModel = new RuleEngineRequestModel
{
ruleList = rules,
customerData = new List<Customer>() { new Customer { Name = "A", Age = 19 },
new Customer { Name = "B", Age = 26 } }.ToArray()
};
session.InsertAll(ruleEngineRequestModel.customerData);
var IspassedorNot = session.Fire();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public static void RuleTestWithInsertDataAll()
{
try
{
CustomRuleRepository repository = new CustomRuleRepository();
List<RuleEngineEntity> rules = new List<RuleEngineEntity>();
rules.Add(new RuleEngineEntity { FieldName = "Age", Name = "CustomerCheck", Value = 25 });
repository.LoadRuleForTest2(rules.FirstOrDefault());
//Compile rules
var factory = repository.Compile();
//Create a working session
var session = factory.CreateSession();
RuleEngineRequestModel ruleEngineRequestModel = new RuleEngineRequestModel
{
ruleList = rules,
customerData = new List<Customer>() { new Customer { Name = "A", Age = 28 },
new Customer { Name = "B", Age = 26 } }.ToArray()
};
session.InsertAll(ruleEngineRequestModel.customerData);
var IspassedorNot = session.Fire();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
public class RuleEngineRequestModel
{
public List<RuleEngineEntity> ruleList { get; set; }
public Customer[] customerData { get; set; }
public List<Customer> customerDataList { get; set; }
}
public class RuleEngineEntity
{
public string Name { get; set; }
public int Value { get; set; }
public string Operator { get; set; }
public string FieldName { get; set; }
public bool SendEmail { get; set; }
}
public class Customer
{
public string Name { get; set; }
public int Age { get; set; }
}
public class CustomRuleRepository : IRuleRepository
{
private readonly IRuleSet _ruleSet = new RuleSet("customerRule");
public IEnumerable<IRuleSet> GetRuleSets()
{
return new[] { _ruleSet };
}
public void LoadRuleForTest1(RuleEngineEntity rule)
{
_ruleSet.Add(BuildRuleForTest1(rule));
}
public void LoadRuleForTest2(RuleEngineEntity rule)
{
_ruleSet.Add(BuildRuleForTest2(rule));
}
public List<IRuleDefinition> BuildRuleForTest1(RuleEngineEntity rule)
{
return Test1(rule);
}
public List<IRuleDefinition> BuildRuleForTest2(RuleEngineEntity rule)
{
return Test2(rule);
}
public List<IRuleDefinition> Test1(RuleEngineEntity rule)
{
RuleBuilder builder = new RuleBuilder();
builder.Name("DefaultRules");
try
{
var modelPattern = builder.LeftHandSide().Pattern(typeof(Customer), "CustomerCheck");
var modelParameter = modelPattern.Declaration.ToParameterExpression();
var expres = Expression.Property(modelParameter, rule.FieldName);
var binaryExpression = Expression.GreaterThan(expres, Expression.Constant(rule.Value));
LambdaExpression expressionCondition = Expression.Lambda(binaryExpression,
modelParameter);
modelPattern.Condition(expressionCondition);
Expression<Action<IContext, Customer, RuleEngineEntity>> action =
(ctx, CustomerCheck, rules) => FireActionAsync(ctx, CustomerCheck, rules);
builder.RightHandSide().Action(action);
}
catch (Exception e)
{
// throw new Exception(e.Message);
}
var buildRule = builder.Build();
return new List<IRuleDefinition> { buildRule };
}
public List<IRuleDefinition> Test2(RuleEngineEntity rule)
{
RuleBuilder builder = new RuleBuilder();
builder.Name("DefaultRules");
try
{
var modelPattern = builder.LeftHandSide().Pattern(typeof(RuleEngineRequestModel), "CustomerCheck");
var modelParameter = modelPattern.Declaration.ToParameterExpression();
var customerDataInArray = Expression.Property(modelParameter, nameof(RuleEngineRequestModel.customerData));
var indx = Expression.Parameter(typeof(int), "index");
var customerData = Expression.ArrayIndex(customerDataInArray, indx);
var expres = Expression.Property(customerData, rule.FieldName);
var binaryExpression = Expression.GreaterThan(expres, Expression.Constant(rule.Value));
LambdaExpression expressionCondition = Expression.Lambda(binaryExpression,
modelParameter);
modelPattern.Condition(expressionCondition);
Expression<Action<IContext, Customer>> action =
(ctx, CustomerCheck) => FireActionAsync(ctx, CustomerCheck, null);
builder.RightHandSide().Action(action);
}
catch (Exception e)
{
// throw new Exception(e.Message);
}
var buildRule = builder.Build();
return new List<IRuleDefinition> { buildRule };
}
public void FireActionAsync(IContext ctx, Customer customer, RuleEngineEntity rule=null)
{
Console.WriteLine($"{rule.Name} Triggered");
}
}
Error: variable 'index' of type 'System.Int32' referenced from scope '', but it is not defined
Expected: want to validate rule against array data with dynamic indexer.
At a quick glance, it appears that you are passing in an array of Customers when inserting the facts into the session, but the rule is looking for the RuleEngineRequestModel.
Also, side note - why are you initialising an array as a List, then converting to an array, rather than just initialising as an array? i.e.
var ruleEngineRequestModel = new RuleEngineRequestModel
{
ruleList = rules,
customerData = {
new Customer { Name = "A", Age = 28 },
new Customer { Name = "B", Age = 26 }
}
};
Finally, why are you inserting the rules at the same time as the data? That seems like it would cause more headaches than benefits, especially seeing as your runtime rule builder ignores them entirely.
EDIT: Having had a chance to see the updated code, this confirms my suspicions - if you use Rule 1 for both tests, it should work (rather, it will fire for each individual customer). The reason why Rule 2 never works is because it's expecting a RuleEngineRequestModel, but you are passing the IEnumerable in directly. Either pass in the request model directly and continue to use Rule 2, or scrap Rule 2 entirely.

Custom fields with FormBuilder in the Microsoft Bot Framework - not working

I tried this solution: Custom fields with FormBuilder in the Microsoft Bot Framework
But failed to get it working....The problem I encountered is that when I assign the base.Form = value, the _prompt in the _field gets a default recognizer, and it won't get overriden in the next line's SetRecognizer call, that only replaces the _field's recognizer.
However the matching process uses the _prompt's recognizer internally ( ? ).
Here is my code:
public class LuisIntentRecognizer<T> : RecognizePrimitive<T>
where T : class
{
public LuisIntentRecognizer(IField<T> field, string luisModelID, string luisSubscriptionKey)
: base(field)
{
_luisModelID = luisModelID;
_luisSubscriptionKey = luisSubscriptionKey;
}
public override DescribeAttribute ValueDescription(object value)
{
return new DescribeAttribute((string)value);
}
public override IEnumerable<string> ValidInputs(object value)
{
yield return (string)value;
}
public override TermMatch Parse(string input)
{
TermMatch result = null;
if (!string.IsNullOrWhiteSpace(input))
{
var luisModel = new LuisModelAttribute(_luisModelID, _luisSubscriptionKey);
var luisService = new LuisService(luisModel);
var luisResult = luisService.QueryAsync(input).Result; // TODO refactor somehow to async
var winner = luisResult.Intents.MaxBy(i => i.Score ?? 0d);
if (winner != null && !string.IsNullOrEmpty(winner.Intent))
{
result = new TermMatch(0, winner.Intent.Length, 0.0, winner.Intent);
}
else
{
result = new TermMatch(0, input.Length, 0.0, input);
}
}
return result;
}
public override string Help(T state, object defaultValue)
{
var prompt = new Prompter<T>(_field.Template(TemplateUsage.StringHelp), _field.Form, null);
var args = HelpArgs(state, defaultValue);
return prompt.Prompt(state, _field.Name, args.ToArray()).Prompt;
}
private string _luisModelID;
private string _luisSubscriptionKey;
}
public class LuisIntentField<T> : FieldReflector<T>
where T : class
{
public LuisIntentField(string name, string luisModelID, string luisSubscriptionKey, bool ignoreAnnotations = false)
: base(name, ignoreAnnotations)
{
_luisModelID = luisModelID;
_luisSubscriptionKey = luisSubscriptionKey;
}
public override IForm<T> Form
{
set
{
base.Form = value;
base.SetRecognizer(new LuisIntentRecognizer<T>(this, _luisModelID, _luisSubscriptionKey));
}
}
private string _luisModelID;
private string _luisSubscriptionKey;
}
Could anyone get it working?
Thanks
It seems to be a bug in the framework indeed: https://github.com/Microsoft/BotBuilder/issues/879

How to restrict the file types in FileUpload in MVC3?

I have a fileupload function where users can upload files. I want to restrict the users from upload certain file types. The types allowed are: .doc,.xlsx,.txt,.jpeg.
How I can do this?
This is my actual file upload code:
public ActionResult UploadFile(string AttachmentName, BugModel model)
{
BugModel bug = null;
if (Session["CaptureData"] == null)
{
bug = model;
}
else
{
bug = (BugModel)Session["CaptureData"];
}
foreach (string inputTagName in Request.Files)
{
HttpPostedFileBase file1 = Request.Files[inputTagName];
if (file1.ContentLength > 0)
{
string path = "/Content/UploadedFiles/" + Path.GetFileName(file1.FileName);
string savedFileName = Path.Combine(Server.MapPath("~" + path));
file1.SaveAs(savedFileName);
BugAttachment attachment = new BugAttachment();
attachment.FileName = "~" + path.ToString();
attachment.AttachmentName = AttachmentName;
attachment.AttachmentUrl = attachment.FileName;
bug.ListFile.Add(attachment);
model = bug;
Session["CaptureData"] = model;
}
}
ModelState.Clear();
return View("LoadBug", bug);
}
The first thing to verify is whether the file extension contained in file1.FileName matches one of the allowed extensions. Then if you really want to ensure that the user hasn't renamed some other file type to an allowed extension you will need to look into the contents of the file to recognize whether it is one of the allowed types.
Here's an example how to check whether the file extension belongs to a list of predefined extensions:
var allowedExtensions = new[] { ".doc", ".xlsx", ".txt", ".jpeg" };
var extension = Path.GetExtension(file1.FileName);
if (!allowedExtensions.Contains(extension))
{
// Not allowed
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AllowedFileExtensionAttribute : ValidationAttribute
{
public string[] AllowedFileExtensions { get; private set; }
public AllowedFileExtensionAttribute(params string[] allowedFileExtensions)
{
AllowedFileExtensions = allowedFileExtensions;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var file = value as HttpPostedFileBase;
if (file != null)
{
if (!AllowedFileExtensions.Any(item => file.FileName.EndsWith(item, StringComparison.OrdinalIgnoreCase)))
{
return new ValidationResult(string.Format("{1} için izin verilen dosya uzantıları : {0} : {2}", string.Join(", ", AllowedFileExtensions), validationContext.DisplayName, this.ErrorMessage));
}
}
return null;
}
}
Usage In Model
[AllowedFileExtension(".jpg", ".png", ".gif", ".jpeg")]
public HttpPostedFileBase KategoriResmi { get; set; }
You can use the ContentType property of the HttpPostedFileBase for a basic check of the file type (mime type): See MSDN's page on the Content-Type property here
Here is one way to do it:
private static bool IsValidContentType(string contentType)
{
string ct = contentType.ToLower();
return ((ct == "application/msword") || (ct == "application/pdf") || (ct == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
}
etc..
However, for a deeper inspection, you will have to inspect the file content. It's easy to change a file extension..

ASP.NET MVC3 Physical Location of View from controller

What is the proper way to get the physical location of the View that will be served by a MVC action from inside the action?
I need the last modified time of the file for sending response headers.
The proper way to the get physical location of a view is to map its virtual path. The virtual path can be retrieved from the ViewPath property of BuildManagerCompiledView (RazorView derive from that class, and your IView instances will therefore typically have that property).
Here is an extension method that you can use:
public static class PhysicalViewPathExtension
{
public static string GetPhysicalViewPath(this ControllerBase controller, string viewName = null)
{
if (controller == null)
{
throw new ArgumentNullException("controller");
}
ControllerContext context = controller.ControllerContext;
if (string.IsNullOrEmpty(viewName))
{
viewName = context.RouteData.GetRequiredString("action");
}
var result = ViewEngines.Engines.FindView(context, viewName, null);
BuildManagerCompiledView compiledView = result.View as BuildManagerCompiledView;
if (compiledView != null)
{
string virtualPath = compiledView.ViewPath;
return context.HttpContext.Server.MapPath(virtualPath);
}
else
{
return null;
}
}
}
Use it something like this:
public ActionResult Index()
{
string physicalPath = this.GetPhysicalViewPath();
ViewData["PhysicalPath"] = physicalPath;
return View();
}
or:
public ActionResult MyAction()
{
string physicalPath = this.GetPhysicalViewPath("MyView");
ViewData["PhysicalPath"] = physicalPath;
return View("MyView");
}
That could work:
private DateTime? GetDate(string controller, string viewName)
{
var context = new ControllerContext(Request.RequestContext, this);
context.RouteData.Values["controller"] = controller;
var view = ViewEngines.Engines.FindView(context, viewName, null).View as BuildManagerCompiledView;
var path = view == null ? null : view.ViewPath;
return path == null ? (DateTime?) null : System.IO.File.GetLastWriteTime(path);
}

How to use Razor Section multiple times in a View & PartialView (merge) without overriding it?

In the _Layout.cshtml file, I have a section at the bottom of the body called "ScriptsContent" declared like this:
#RenderSection("ScriptsContent", required: false)
In my view, I can then use this section to add scripts to be executed. But what if I also have a PartialView that also need to use this section to add additional scripts?
View
#section ScriptsContent
{
<script type="text/javascript">
alert(1);
</script>
}
#Html.Partial("PartialView")
PartialView
#section ScriptsContent
{
<script type="text/javascript">
alert(2);
</script>
}
Result
Only the first script is rendered. The second script doesn't exist in source code of the webpage.
Razor seems to only output the first #section ScriptsContent that it sees. What I would like to know is if there's a way to merge each call to the section.
If we cannot do this, what do you propose?
Here's a solution for that problem. It's from this blog: http://blog.logrythmik.com/post/A-Script-Block-Templated-Delegate-for-Inline-Scripts-in-Razor-Partials.aspx
public static class ViewPageExtensions
{
private const string SCRIPTBLOCK_BUILDER = "ScriptBlockBuilder";
public static MvcHtmlString ScriptBlock(this WebViewPage webPage, Func<dynamic, HelperResult> template)
{
if (!webPage.IsAjax)
{
var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(template(null).ToHtmlString());
webPage.Context.Items[SCRIPTBLOCK_BUILDER] = scriptBuilder;
return new MvcHtmlString(string.Empty);
}
return new MvcHtmlString(template(null).ToHtmlString());
}
public static MvcHtmlString WriteScriptBlocks(this WebViewPage webPage)
{
var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();
return new MvcHtmlString(scriptBuilder.ToString());
}
}
so anywwhere in your View or PartialView you can use this:
#this.ScriptBlock(
#<script type='text/javascript'>
alert(1);
</script>
)
and in your _Layout or MasterView, use this:
#this.WriteScriptBlocks()
There is no way to share sections between a view and partial views.
Absent a ScriptManager-like solution, you could have a collection of script files (initialized in your view and stored either in HttpContext.Items or in ViewData) to which the partial view would append the script file names it requires. Then towards the end of your view you would declare a section that fetches that collection and emits the right script tags.
The problem with the accepted answer is that it breaks Output Caching. The trick to solving this is to overwrite the OutputCache attribute with your own implementation. Unfortunately we can't extend the original attribute since it has lots of internal methods which we need to access.
I actually use Donut Output Caching which overwrites the OutputCache attribute itself. There are alternative libraries which also use their own OutputCache attribute so I will explain the steps I made to get it to work so that you can apply it to whichever one you're using.
First you need to copy the existing OutputCache attribute and place it within your application. You can get the existing attribute by looking at the source code.
Now add the following property to the class. This is where we store the script blocks so we can render the correct ones when retrieving from the cache.
public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();
Now inside the OnActionExecuting method you need to store the cache key (the unique identifier for the output cache) inside the current requests collection. For example:
filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;
Now modify the ViewPageExtensions class by adding the following (replacing CustomOutputCacheAttribute with the name of your attribute):
var outputCacheKey = webPage.Context.Items["OutputCacheKey"] as string;
if (outputCacheKey != null)
CustomOutputCacheAttribute.ScriptBlocks.AddOrUpdate(outputCacheKey, new StringBuilder(template(null).ToHtmlString()), (k, sb) => {
sb.Append(template(null).ToHtmlString());
return sb;
});
before:
return new MvcHtmlString(string.Empty);
Note: For a slight performance boost you'll also want to make sure you only call "template(null).ToHtmlString()" once.
Now return to your custom OutputCache attribute and add the following only when you are retrieving from the cache inside the OnActionExecuting method:
if (ScriptBlocks.ContainsKey(cacheKey)) {
var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());
filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
}
Here's the final code of my attribute:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;
using DevTrends.MvcDonutCaching;
public class CustomOutputCacheAttribute : ActionFilterAttribute, IExceptionFilter {
private readonly IKeyGenerator _keyGenerator;
private readonly IDonutHoleFiller _donutHoleFiller;
private readonly IExtendedOutputCacheManager _outputCacheManager;
private readonly ICacheSettingsManager _cacheSettingsManager;
private readonly ICacheHeadersHelper _cacheHeadersHelper;
private bool? _noStore;
private CacheSettings _cacheSettings;
public int Duration { get; set; }
public string VaryByParam { get; set; }
public string VaryByCustom { get; set; }
public string CacheProfile { get; set; }
public OutputCacheLocation Location { get; set; }
public bool NoStore {
get { return _noStore ?? false; }
set { _noStore = value; }
}
public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();
public DonutOutputCacheAttribute() {
var keyBuilder = new KeyBuilder();
_keyGenerator = new KeyGenerator(keyBuilder);
_donutHoleFiller = new DonutHoleFiller(new EncryptingActionSettingsSerialiser(new ActionSettingsSerialiser(), new Encryptor()));
_outputCacheManager = new OutputCacheManager(OutputCache.Instance, keyBuilder);
_cacheSettingsManager = new CacheSettingsManager();
_cacheHeadersHelper = new CacheHeadersHelper();
Duration = -1;
Location = (OutputCacheLocation)(-1);
}
public override void OnActionExecuting(ActionExecutingContext filterContext) {
_cacheSettings = BuildCacheSettings();
var cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);
if (_cacheSettings.IsServerCachingEnabled) {
var cachedItem = _outputCacheManager.GetItem(cacheKey);
if (cachedItem != null) {
filterContext.Result = new ContentResult {
Content = _donutHoleFiller.ReplaceDonutHoleContent(cachedItem.Content, filterContext),
ContentType = cachedItem.ContentType
};
if (ScriptBlocks.ContainsKey(cacheKey)) {
var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());
filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
}
}
}
if (filterContext.Result == null) {
filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;
var cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
var originalWriter = filterContext.HttpContext.Response.Output;
filterContext.HttpContext.Response.Output = cachingWriter;
filterContext.HttpContext.Items[cacheKey] = new Action<bool>(hasErrors => {
filterContext.HttpContext.Items.Remove(cacheKey);
filterContext.HttpContext.Response.Output = originalWriter;
if (!hasErrors) {
var cacheItem = new CacheItem {
Content = cachingWriter.ToString(),
ContentType = filterContext.HttpContext.Response.ContentType
};
filterContext.HttpContext.Response.Write(_donutHoleFiller.RemoveDonutHoleWrappers(cacheItem.Content, filterContext));
if (_cacheSettings.IsServerCachingEnabled && filterContext.HttpContext.Response.StatusCode == 200)
_outputCacheManager.AddItem(cacheKey, cacheItem, DateTime.UtcNow.AddSeconds(_cacheSettings.Duration));
}
});
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext) {
ExecuteCallback(filterContext, false);
if (!filterContext.IsChildAction)
_cacheHeadersHelper.SetCacheHeaders(filterContext.HttpContext.Response, _cacheSettings);
}
public void OnException(ExceptionContext filterContext) {
if (_cacheSettings != null)
ExecuteCallback(filterContext, true);
}
private void ExecuteCallback(ControllerContext context, bool hasErrors) {
var cacheKey = _keyGenerator.GenerateKey(context, _cacheSettings);
var callback = context.HttpContext.Items[cacheKey] as Action<bool>;
if (callback != null)
callback.Invoke(hasErrors);
}
private CacheSettings BuildCacheSettings() {
CacheSettings cacheSettings;
if (string.IsNullOrEmpty(CacheProfile)) {
cacheSettings = new CacheSettings {
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally,
Duration = Duration,
VaryByCustom = VaryByCustom,
VaryByParam = VaryByParam,
Location = (int)Location == -1 ? OutputCacheLocation.Server : Location,
NoStore = NoStore
};
} else {
var cacheProfile = _cacheSettingsManager.RetrieveOutputCacheProfile(CacheProfile);
cacheSettings = new CacheSettings {
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally && cacheProfile.Enabled,
Duration = Duration == -1 ? cacheProfile.Duration : Duration,
VaryByCustom = VaryByCustom ?? cacheProfile.VaryByCustom,
VaryByParam = VaryByParam ?? cacheProfile.VaryByParam,
Location = (int)Location == -1 ? ((int)cacheProfile.Location == -1 ? OutputCacheLocation.Server : cacheProfile.Location) : Location,
NoStore = _noStore.HasValue ? _noStore.Value : cacheProfile.NoStore
};
}
if (cacheSettings.Duration == -1)
throw new HttpException("The directive or the configuration settings profile must specify the 'duration' attribute.");
if (cacheSettings.Duration < 0)
throw new HttpException("The 'duration' attribute must have a value that is greater than or equal to zero.");
return cacheSettings;
}
}
I also had to modify the Donut Output Cache library to make IExtendedOutputCacheManager and the OutputCacheManager constructor public.
Please note this has been extracted from my application and may require some minor tweaks. You should also place WriteScriptBlocks at the bottom of the page so it is not called until after all child actions are triggered.
Hope this helps.

Resources