8

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!

4 Answers 4

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);
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

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: stackoverflow.com/a/8937793/140449
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;
}

1 Comment

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
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
    }
}

3 Comments

The question is about updating the result though - this isn't possible with your solution is it?
@Gaz Should be simple enough to modify to make it work. Quite a while ago so can't remember sorry!
@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.
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;
}

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.