7

I've got the following filter in place on an action to capture the HTML output, convert it to a string, do some operations to modify the string, and return a ContentResult with the new string. Unfortunately, I keep ending up with an empty string.

private class UpdateFilter : ActionFilterAttribute
    {
        private Stream stream;

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            stream = filterContext.HttpContext.Response.Filter;
            stream = new MemoryStream();
            filterContext.HttpContext.Response.Filter = stream;
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            StreamReader responsereader = new StreamReader(filterContext.HttpContext.Response.Filter);  //empty stream? why?
            responsereader.BaseStream.Position = 0;
            string response = responsereader.ReadToEnd();
            ContentResult contres = new ContentResult();
            contres.Content = response;
            filterContext.Result = contres;
        }
    }

I've pinned down that StreamReader(stream).ReadToEnd() returns an empty string, but I can't figure out why.

Any ideas how to fix this?

EDIT: I've changed the OnActionExecuted to OnResultExecuted, and now it is called after the View has been generated, but the stream is still empty!

yoozer8
  • 7,361
  • 7
  • 58
  • 93

4 Answers4

12

I solved this by hijacking the HttpWriter, and having it write into a StringBuilder rather than the response, and then doing whatever needs to be done to/with the response before writing it to the output.

private class UpdateFilter : ActionFilterAttribute
{
    private HtmlTextWriter tw;
    private StringWriter sw;
    private StringBuilder sb;
    private HttpWriter output;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        sb = new StringBuilder();
        sw = new StringWriter(sb);
        tw = new HtmlTextWriter(sw);
        output = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output;
        filterContext.RequestContext.HttpContext.Response.Output = tw;
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        string response = sb.ToString();
        //response processing
        output.Write(response);
    }
}

Above code using the HttpContext to avoid threading errors - see jaminto's comment

private class RenderFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        StringBuilder sb = new StringBuilder();
        StringWriter sw = new StringWriter(sb);
        HtmlTextWriter tw = new HtmlTextWriter(sw);
        HttpWriter output = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output;
        filterContext.HttpContext.Items["sb"] = sb;
        filterContext.HttpContext.Items["output"] = output;
        filterContext.RequestContext.HttpContext.Response.Output = tw;
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        string response = filterContext.HttpContext.Items["sb"].ToString();
        //response processing
        ((HttpWriter)filterContext.HttpContext.Items["output"]).Write(response);
    }
}
yoozer8
  • 7,361
  • 7
  • 58
  • 93
  • 15
    a warning: it's not recommended to use instance variables in action filters. you're not guaranteed to get a new instance of the ActionFilterAttribute on each request. i based my code off this answer and got in trouble when it went to production with thousands of requests per second - the wires (threads) were getting crossed. store the instance variables in the filterContext.HttpContext.Items as suggested in this post: http://stackoverflow.com/a/8937793/140449 – jaminto Aug 05 '13 at 22:16
2

Try rewinding the stream to the beginning by setting Position = 0; before you read it.

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
    stream.Position = 0;
    string response = new StreamReader(stream).ReadToEnd();
    ContentResult contres = new ContentResult();
    contres.Content = response;
    filterContext.Result = contres;
}
Jakub Konecki
  • 45,581
  • 7
  • 87
  • 126
  • That didn't fix it, but it did lead me to notice that the Position is already 0. So it looks like the stream must be empty....I wonder why – yoozer8 Aug 17 '11 at 14:51
1

I think I've developed a pretty good way to do this.

  • Replace the Reponse Filter with a custom one
  • This filter takes a delegate to an abstract method which takes a stream
  • This the delegate, and hence the abstract method are called on the close of the stream, i.e. when all the HTML is available
  • Override the OnClose method and play with the stream as you like.

public abstract class ReadOnlyActionFilterAttribute : ActionFilterAttribute
{
    private delegate void ReadOnlyOnClose(Stream stream);

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Filter = new OnCloseFilter(
            filterContext.HttpContext.Response.Filter, 
            this.OnClose);
        base.OnActionExecuting(filterContext);
    }

    protected abstract void OnClose(Stream stream);

    private class OnCloseFilter : MemoryStream
    {
        private readonly Stream stream;

        private readonly ReadOnlyOnClose onClose;

        public OnCloseFilter(Stream stream, ReadOnlyOnClose onClose)
        {
            this.stream = stream;
            this.onClose = onClose;
        }

        public override void Close()
        {
            this.Position = 0;
            this.onClose(this);
            this.Position = 0;
            this.CopyTo(this.stream);
            base.Close();
        }
    }
}

You can then derive from this to another attribute to access the stream and get the HTML:

public class MyAttribute : ReadOnlyActionFilterAttribute
{
    protected override void OnClose(Stream stream)
    {
        var html = new HtmlDocument();
        html.Load(stream);
        // play with html
    }
}
dav_i
  • 27,509
  • 17
  • 104
  • 136
  • The question is about updating the result though - this isn't possible with your solution is it? – Gaz Apr 22 '15 at 10:17
  • @Gaz Should be simple enough to modify to make it work. Quite a while ago so can't remember sorry! – dav_i Apr 22 '15 at 10:22
  • @Gaz, this should get the OP in the right direction. The problem is that the default stream in `filterContext.RequestContext.HttpContext.Response` does not support reading, so you have to replace the stream with one that allows reading. – ps2goat Jun 05 '15 at 15:41
0

Can you verify that stream is not NULL in the OnActionExectuted-method? I'm not sure the state of the stream-variable is being stored through the process..

Why don't you try to get the stream out of the filterContext:

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
    var stream = filterContext.HttpContext.Response.Filter;
    string response = new StreamReader(stream).ReadToEnd();
    ContentResult contres = new ContentResult();
    contres.Content = response;
    filterContext.Result = contres;
}
Pbirkoff
  • 4,642
  • 2
  • 20
  • 18
  • Well, I thought this would be the answer, but I've got the same problem. The stream I get from filterContext.HttpContext.Response.Filter has length 0. – yoozer8 Aug 17 '11 at 14:55