Search code examples
c#asp.net-coremiddlewarerazor-pages

Inject value from middleware into Razor Page HTML body


I have a middleware which calculates Razor Page execution time:

app.Use(async (context, next) =>
{
    var sw = new Stopwatch();
    sw.Start();
    await next.Invoke();
    sw.Stop();
    await context.Response.WriteAsync($"<div class=\"container\">Processing time: {sw.ElapsedMilliseconds} ms<div>");
});

It works fine, however HTML created by WriteAsync is added at the very end of the response, after closing </html> tag.

How can I place - pass or inject - a value of sw.ElapsedMilliseconds calculated by the middleware in specific place inside HTML body? Using ASP.NET Core 6.0.


Solution

  • Maybe you can use the HTML Agility Pack.

    You can use the LoadHtml and HtmlNode.CreateNode methods to create the html element you want to insert, and then use DocumentNode.SelectSingleNode to select the position you want to insert.

    For testing,you can refer to the following code:

    Custom middleware:

    //Install-Package HtmlAgilityPack
    public class ResponseMeasurementMiddleware
    {
            private readonly RequestDelegate _next;
    
            public ResponseMeasurementMiddleware(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task Invoke(HttpContext context)
            {
                var originalBody = context.Response.Body;
                var newBody = new MemoryStream();
                context.Response.Body = newBody;
    
                var watch = new Stopwatch();
                long responseTime = 0;
                watch.Start();
                await _next(context);
               // read the new body
                responseTime = watch.ElapsedMilliseconds;
                newBody.Position = 0;
                var newContent = await new StreamReader(newBody).ReadToEndAsync();
                // calculate the updated html
                var updatedHtml = CreateDataNode(newContent, responseTime);
                // set the body = updated html
                var updatedStream = GenerateStreamFromString(updatedHtml);
                await updatedStream.CopyToAsync(originalBody);
                context.Response.Body = originalBody;
    
            }
            public static Stream GenerateStreamFromString(string s)
            {
                var stream = new MemoryStream();
                var writer = new StreamWriter(stream);
                writer.Write(s);
                writer.Flush();
                stream.Position = 0;
                return stream;
            }
            private string CreateDataNode(string originalHtml, long responseTime)
            {
               var htmlDoc = new HtmlDocument();
                htmlDoc.LoadHtml(originalHtml);
                HtmlNode testNode = HtmlNode.CreateNode($"<div class=\"container\">Processing time: {responseTime.ToString()} ms.</div>");
                var htmlBody = htmlDoc.DocumentNode.SelectSingleNode(".//body/div");
                if (htmlBody != null)
                {
                    htmlBody.InsertBefore(testNode, htmlBody.FirstChild);
                }
    
                string rawHtml = htmlDoc.DocumentNode.OuterHtml; //using this results in a page that displays my inserted HTML correctly, but duplicates the original page content.
                                                                 //rawHtml = "some text"; uncommenting this results in a page with the correct format: this text, followed by the original contents of the page
    
                return rawHtml;
            }
    }
    

    htmlDoc.DocumentNode.SelectSingleNode(".//body/div") and htmlBody.InsertBefore(testNode, htmlBody.FirstChild) can customize where you want to insert.

    And use custom middleware:

    app.UseMiddleware<ResponseMeasurementMiddleware>();
    

    Test Result:

    enter image description here