Pages

3/18/2022

ASP.NET Core Middleware to Update Response Body

I needed a quick way to update a mix of static HTML files and Razor views to change a message common to a lot of pages. In the example here I will update the Copyright message in HTML pages.

As usual I searched the web and found many examples, none that worked the way I needed (or worked at all!), but I did find an assortment of "bits" scattered throughout StackOverflow that helped me build a working solution.

At first glance this should have been a textbook example of ASP.NET Core middleware. Intercept the outgoing response, update the body HTML and forward on through the middleware pipeline. The first issue was the Stream object used by Response.Body object, its an Microsoft.WebTools.BrowserLink.Net.ScriptInjectionFilterStream object that does not support Seek. The first "bit" that I found in StackOverflow was to replace the Response.Body steam with my own stream, in particular a MemoryStream. Then we can send the Response object on down the middleware pipeline using next.Invoke. When the Response comes back to us though the pipeline we need to extract the HTML text from our custom stream object, do our updates to the HTML and finally get the original kind of stream back into the Response.Body with our changes.

The next problem was that I could not find a way to clear or reset the stream that represented the returned Body content. I was getting both the original page plus the updated page, i.e. my data was getting appended. .SetLength(0) and .Seek(0, SeekOrigin.Begin) did not work. ("Specified method is not supported"). StackOverflow to the rescue again. A quick solution was to recreate an empty stream object. ( context.Response.Body = new MemoryStream(); )

Remember! The order of middleware is important. If you want this to process your JavaScript, CSS and static HTML files, then add this before app.UseStaticFiles(). Most likely order:

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.Use(  the code below  );
            app.UseRouting();
            app.UseAuthorization();

 

So here's the solution as an inline "app.Use()" block added to Startup.cs or Program.cs (.NET 6). It replaces the Copyright date from "2021" to the current year.

 

Note: Code tested in .NET Core 3.1 and .NET 6.0 using Visual Studio 2022.


app.Use(async (context, next) =>
{
    using (var replacementStream = new MemoryStream())
    {
        // preprocessing the Body
        // replace {Microsoft.WebTools.BrowserLink.Net.ScriptInjectionFilterStream}
        // with a MemoryStream (seekable!)
        System.IO.Stream originalStream = context.Response.Body;
        context.Response.Body = replacementStream;

        // do any preprocessing of the Request or Response object here
        // none needed for this example

        // move on down the pipeline, but with our custom stream
        await next.Invoke();

        // we are now back from the rest of the pipeline and on the way out (TTN)

        // postprocessing the Body

        // read the returned Body
        replacementStream.Seek(0, SeekOrigin.Begin);
        using (var bufferReader = new StreamReader(replacementStream))
        {
            string body = await bufferReader.ReadToEndAsync();

            // Is this a web page?
            if (context.Response.ContentType.Contains("text/html"))
            {
                // make changes to the returned HTML
                // set the copyright to the current year
                body = body.Replace("© 2021", "© " + DateTime.Now.Year.ToString());
            }

            // discard anything in the existing body.                        
            context.Response.Body = new MemoryStream();

            // write the updated Body into the Response using our custom stream
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            await context.Response.WriteAsync(body);

            // extract the Body stream into the original steam object
            //   in effect, covert from MemoryStream to 
            //   {Microsoft.WebTools.BrowserLink.Net.ScriptInjectionFilterStream}
            context.Response.Body.Position = 0;
            await context.Response.Body.CopyToAsync(originalStream);

            // now send put the original stream back into the Response
            // and exit to the rest of the middleware pipeline
            context.Response.Body = originalStream;
        }
    }
});


 

Here's the same solution as a Middleware class.

  1. Add the class below.
  2. Update Startup.cs or Program.cs (.NET 6) to add an app.UseReplaceText line.

 Startup.cs or Program.cs:

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseReplaceText();  // custom middleware
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => ....

The class:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.IO;
using System.Threading.Tasks;

namespace WebAppToDemoMiddleware
{
    public class ReplaceTextMiddleware
    {
        private readonly RequestDelegate _next;

        public ReplaceTextMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        // 
        public async Task InvokeAsync(HttpContext context)
        {
            using (var replacementStream = new MemoryStream())
            {
                // preprocessing the Body
                // replace {Microsoft.WebTools.BrowserLink.Net.ScriptInjectionFilterStream}
                // with a MemoryStream (seekable!)
                System.IO.Stream originalStream = context.Response.Body;
                context.Response.Body = replacementStream;

                // do any preprocessing of the Request or Response object here
                // none needed for this example

                // move on down the pipeline, but with our custom stream
                await _next(context);

                // we are now back from the rest of the pipeline and on the way out

                // postprocessing the Body

                // read the returned Body
                replacementStream.Seek(0, SeekOrigin.Begin);
                using (var replacementStreamReader = new StreamReader(replacementStream))
                {
                    string body = await replacementStreamReader.ReadToEndAsync();

                    //if (body.Contains("Welcome"))
                    // Is this a web page?
                    if (context.Response.ContentType.Contains("text/html"))
                    {
                        // make changes to the returned HTML
                        // set the copyright to the current year
                        body = body.Replace("© 2021", "© " + DateTime.Now.Year.ToString());
                    }

                    // discard anything in the existing body.                        
                    context.Response.Body = new MemoryStream();

                    // write the updated Body into the Response using our custom stream
                    context.Response.Body.Seek(0, SeekOrigin.Begin);
                    await context.Response.WriteAsync(body);

                    // extract the Body stream into the original steam object
                    //   in effect, covert from MemoryStream to 
                    //   {Microsoft.WebTools.BrowserLink.Net.ScriptInjectionFilterStream}
                    context.Response.Body.Position = 0;
                    await context.Response.Body.CopyToAsync(originalStream);

                    // now send put the original stream back into the Response
                    // and exit to the rest of the middleware pipeline
                    context.Response.Body = originalStream;
                }
            }

        }
    }

    // Add an extension class so we can use this as app.UseReplaceText()
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseReplaceText(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ReplaceTextMiddleware>();
        }
    }
}
..

No comments:

Post a Comment

Note to spammers...
Spammers, don't waste your time... all posts are moderated. If your comment includes unrelated links, is advertising, or just pure spam, it will never be seen.