Action Chaining is using the output of action A as input for action B. Applying this pattern to ASP.NET MVC projects is not trivial. This post is meant as a quick start.
- client places order in browser
- system redirects client to ~/Order/ThankYou
- system sends email to client with summary about order
- system shows summary of order to client
There are other usecases in which Action Chaining would be a valid pattern, for example in cases where you want to show the output of one action as PDF or as HTML depending on the required format. Apache Cocoon is based on this pattern.
The way to execute an action and capture the output in ASP.NET MVC involves some hijacking of the Response Stream and the RouteValues.
The Response Stream can messed with using a ResponseFilter. A response filter is a class that inherits from stream and takes a stream as constructor argument. The stream in the constructor is the original Response stream (or another filter, they can be chained). If you set the filter, the filter stream gets the writes and is supposed to pass the writes to the wrapped stream. Which is exactly what we won't do: we jealously keep the bytes to ourselves in a memorystream:
class BufferingMemoryStreamFilter : MemoryStreamControllers react on the routevalues to find the right action and view. Because we will be executing another action (perhaps on another controller also), the routevalues should be changed for the occasion and restored afterwards.
{
public BufferingMemoryStreamFilter(Stream wrappedStream)
{
// ignore the wrapped stream
}
}
The method that execute and capture an arbitrary action on an arbitrary controller is posted below.
string GetActionOutput(string controller, string action)Include this in a (base) controller or in a class that has access to the controller to be able to use it.
{
// hijack the response stream
var orgResponseFilter = HttpContext.Response.Filter;
var memoryStreamFilter = new BufferingMemoryStreamFilter(
HttpContext.Response.Filter);
HttpContext.Response.Filter = memoryStreamFilter;
// hijack routeData
var routeData = ControllerContext.RequestContext.RouteData;
var orgAction = routeData.Values["action"];
var orgController = routeData.Values["controller"];
routeData.Values["action"] = action;
routeData.Values["controller"] = controller;
var c = ControllerBuilder.Current
.GetControllerFactory()
.CreateController(ControllerContext.RequestContext, controller);
c.Execute(ControllerContext.RequestContext);
HttpContext.Response.Flush();
memoryStreamFilter.Position = 0;
string result;
using (var r = new StreamReader(memoryStreamFilter))
result = r.ReadToEnd();
// restore
HttpContext.Response.Filter = orgResponseFilter;
routeData.Values["action"] = orgAction;
routeData.Values["controller"] = orgController;
return result;
}
The code in this post is licensed under the Apache 2.0 license, which in practice means you have permission to use it.
6 comments:
Great article, thank you
Interesting approach. When trying to implement, I'm getting and exception thrown wen trying to execute the 'redirected' controller action. Here's the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.IO;
using System.Text;
namespace ResponseCaptureDemo.Controllers
{
[HandleError]
public class HomeController : Controller
{
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index()
{
string output = 'some html here'
return Content(output, "text/html");
}
//[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Redirected()
{
StringBuilder output = new StringBuilder(GetActionOutput("HomeController", "SampleAction"));
output.Append("some additional text here");
return Content(output.ToString(), "text/html");
}
public ActionResult SampleAction()
{
string output = 'some other html here';
return Content(output, "text/html");
}
private string GetActionOutput(string controller, string action)
{
//grab original response stream
var origResponseFilter = HttpContext.Response.Filter;
//replace original filter with empty filter
var memoryStreamFilter = new ResponseCaptureDemo.Utilities.BufferedMemoryStreamFilter(HttpContext.Response.Filter);
HttpContext.Response.Filter = memoryStreamFilter;
//grab original route data
var routeData = ControllerContext.RequestContext.RouteData;
var origAction = routeData.Values["action"];
var origController = routeData.Values["controller"];
//re-route to desired controller and action
routeData.Values["action"] = action;
routeData.Values["controller"] = controller;
//create desired controller and execute
var c = ControllerBuilder.Current.GetControllerFactory().CreateController(ControllerContext.RequestContext, controller); <-- EXCEPTION HERE!
c.Execute(ControllerContext.RequestContext);
HttpContext.Response.Flush();
memoryStreamFilter.Position = 0;
string result;
using (var r = new StreamReader(memoryStreamFilter))
{
result = r.ReadToEnd();
}
//restore
HttpContext.Response.Filter = origResponseFilter;
routeData.Values["action"] = origAction;
routeData.Values["controller"] = origController;
return result;
}
}
}
EXCEPTION - The controller for path '/Home/Redirected' could not be found or it does not implement IController.
Any thoughts?
Sorry I missed your comment until now. Maybe an obvious one, but does the Url Home/Redirected give the expected results if called manually?
The above code works fine. Just remove the Controller keyword from this line
StringBuilder(GetActionOutput("HomeController", "SampleAction"));
It should be StringBuilder(GetActionOutput("Home", "SampleAction"));
Unfortunately HttpContext.Response.Flush(); flushes the headers, which means that we no longer control that part of response :(
Thank u...
Post a Comment