Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The best place to replace text in asp.net mvc pipline


VERSION 2

I have updated the original code taking into account the fact that the write method streamed the HTML from the page in chunks.

As pointed out 'Since you're not guaranteed to have "THE_PLACEHOLDER" being written in a contiguous block of bytes in write. You may get "THE_PLACEH" at the end of one call to write, and "OLDER" at the beginning of the next.

I have fixed this by putting the complete content of the stream in a Stringbuilder and doing any update that is required on the Close method.

Having done this I am asking the same question again below....


I'm working on a CMS that simply replaces a placeholder with the CMS text.

I have the following which is working as it should.

I have overridden the IHttpModule

public class CmsFilterHttpModule : IHttpModule {

  // In the Init method, register HttpApplication events by adding event handlers.
  public void Init( HttpApplication httpApplication ) {

    httpApplication.ReleaseRequestState += new EventHandler( this.HttpApplication_OnReleaseRequestState );

  }

  /// <summary>
  /// HttpApplication_OnReleaseRequestState event handler.
  /// 
  /// Occurs after ASP.NET finishes executing all request event handlers. 
  /// This event causes state modules to save the current state data.
  /// </summary>
  private void HttpApplication_OnReleaseRequestState( Object sender, EventArgs e ) {

    HttpResponse httpResponse = HttpContext.Current.Response;

    if ( httpResponse.ContentType == "text/html" ) {

      httpResponse.Filter = new CmsFilterStream( httpResponse.Filter );

    }

  }

  public void Dispose() {

    //Empty

  }

} 

and the MemoryStream

public class CmsFilterStream : MemoryStream {

  private Stream        _responseStream;  
  private StringBuilder _responseHtml;   

  public CmsFilterStream( Stream inputStream ) {

    _responseStream = inputStream;
    _responseHtml = new StringBuilder();

  }

  /// <summary>
  ///   Writes a block of bytes to the current stream using data read from a buffer.
  /// </summary>
  /// <param name="buffer">The buffer to write data from.</param>
  /// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
  /// <param name="count">The maximum number of bytes to write.</param>
  public override void Write( Byte[] buffer, Int32 offset, Int32 count ) {

    if ( buffer == null ) { throw new ArgumentNullException( "buffer", "ArgumentNull_Buffer" ); }
    if ( offset < 0 ) { throw new ArgumentOutOfRangeException( "offset", "ArgumentOutOfRange_NeedNonNegNum" ); }
    if ( count < 0 ) { throw new ArgumentOutOfRangeException( "count", "ArgumentOutOfRange_NeedNonNegNum" ); }
    if ( buffer.Length - offset < count ) { throw new ArgumentException( "Argument_InvalidOffLen" ); }

    String bufferContent = UTF8Encoding.UTF8.GetString( buffer, offset, count );

    _responseHtml.Append( bufferContent );

  }

  public override void Close() {

    _responseHtml.Replace( "THE_PLACEHOLDER", "SOME_HTML" );

    _responseStream.Write( UTF8Encoding.UTF8.GetBytes( _responseHtml.ToString() ), 0, UTF8Encoding.UTF8.GetByteCount( _responseHtml.ToString() ) );

    _responseStream.Dispose();

    base.Close();

  }

}

and the following in the Web.config

<system.webServer>
  <modules>
    <remove name="CmsFilterHttpModule" />
    <add name="CmsFilterHttpModule" type="{MY_NAMESPACE}.CmsFilterHttpModule" />
  </modules>
</system.webServer>

This does work as I require.

My question really is this the best place in the pipeline to do this before I start to work backwards.

This method is replacing text on the completed output.

I'm looking for the fastest way to replace this text from the pipeline perspective.

For the moment ignoring the speed of String.Replace / Stringbuilder and the various other methods. I see that optimization slightly further on.

I haven't debugged through the whole pipeline yet but though I'm guessing this the page must be being built from different parts i.e. layouts, views partial etc etc. maybe its faster to replace the text at these parts.

Also in addition will there be any issues with

String bufferContent = UTF8Encoding.UTF8.GetString(buffer);

when using other languages Japanese, Chinese etc.

I also must add that I'm trying to do this as a separate added on piece of code that touches the users site MVC code as little as possible.

like image 874
William Humphreys Avatar asked Nov 01 '22 08:11

William Humphreys


1 Answers

To handle the complete response without the fuzz of having to deal with state between calls to write your implementation doesn't need to override the Write method, only the Close method is needed because you first need to capture ALL bytes before converting. This is an implementation that works:

public class CmsFilterStream : MemoryStream {

    private Stream        _responseStream;  

    public CmsFilterStream( Stream inputStream ) {
        _responseStream = inputStream;
    }

    public override void Close() {
        var allHtml = UTF8Encoding.UTF8.GetString(this.ToArray()); // get ALL bytes!!
        allHtml = allHtml.Replace("THE_PLACEHOLDER", "SOME_HTML");

        var buf =UTF8Encoding.UTF8.GetBytes(allHtml);
        _responseStream.Write(buf,0, buf.Length);

        _responseStream.Flush(); // I assume the caller will close the _responseStream

        base.Close();
    }
}

This is a naive implementation. You can optimize the replacement code and writing to the stream but I would only optimize that if your performance measurements indicate that this peace of code is on the hot path.

like image 178
rene Avatar answered Nov 15 '22 04:11

rene