3

I've been trying to cobble up some middleware that will allow me to measure the processing time on a request. This example gave me a good starting point, but I've run into trouble.

In the code below, I am able to measure the process time and insert it in a div (using HTML Agility Pack). However, the original contents of the page get duplicated. I think I'm doing something incorrectly with the context.Response.Body property in UpdateHtml(), but cannot figure out what it is. (I made some comments in the code.) If you see anything that looks incorrect, could you please let me know?

Thanks.

public class ResponseMeasurementMiddleware
{
    private readonly RequestDelegate _next;

    public ResponseMeasurementMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var watch = new Stopwatch();
        watch.Start();
        context.Response.OnStarting(async () =>
        {
            var responseTime = watch.ElapsedMilliseconds;
            var newContent = string.Empty;
            var existingBody = context.Response.Body;
            string updatedHtml = await UpdateHtml(responseTime, context);

            await context.Response.WriteAsync(updatedHtml);
        });

        await _next.Invoke(context);
    }

    private async Task<string> UpdateHtml(long responseTime, HttpContext context)
    {
        var newContent = string.Empty;
        var existingBody = context.Response.Body;
        string updatedHtml = "";
        //I think I'm doing something incorrectly in this using...
        using (var newBody = new MemoryStream())
        {
            context.Response.Body = newBody;

            await _next(context);

            context.Response.Body = existingBody;
            newBody.Position = 0;

            newContent = await new StreamReader(newBody).ReadToEndAsync();
            updatedHtml = CreateDataNode(newContent, responseTime);
        }

        return updatedHtml;
    }

    private string CreateDataNode(string originalHtml, long responseTime)
    {
        var htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(originalHtml);
        HtmlNode testNode = HtmlNode.CreateNode($"<div><h2>Inserted using Html Agility Pack: Response Time: {responseTime.ToString()} ms.</h2><div>");
        var htmlBody = htmlDoc.DocumentNode.SelectSingleNode("//body");
        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;
    }
}

1 Answer 1

9

For duplicated html, it is caused by await _next(context); in UpdateHtml which will invoke the rest middlware like MVC to handle the requests and response.

Withtout await _next(context);, you should not modify the Reponse body in context.Response.OnStarting.

For a workaround, I would suggest you place the ResponseMeasurementMiddleware as the first middleware and then calculate the time like

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
        // 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><h2>Inserted using Html Agility Pack: Response Time: {responseTime.ToString()} ms.</h2><div>");
        var htmlBody = htmlDoc.DocumentNode.SelectSingleNode("//body");
        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;
    }
}

And register ResponseMeasurementMiddleware like

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMiddleware<ResponseMeasurementMiddleware>();
    //rest middlwares
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

For this way app.UseMiddleware<ResponseMeasurementMiddleware>();, the action will be last opertion before sending the response, and then processing time would be suitable for processing time.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you! You saved a lot of time for me. Is it a problem, that the MemoryStream doesn't get closed anywhere? I did it with a using-statement before, but got every time the error message that "few bytes are written (0 of x)". I think it was, because the MemoryStream got closed after the using statement ...

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.