4

I have a simple .net core app that emits an API output.

My Configure method is pretty simple :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env  )
        {

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }

This is the current output from the API :

enter image description here

Just for testing purpose, I want to add HTML tag before and after the response :

Something like ( edited manually in DOM ) :

enter image description here

So I've added this :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env  )
        {
            

          app.Use(async (context, next) =>
         {
             await context.Response.WriteAsync("<b> Hi</b>");
             await next ();
             await context.Response.WriteAsync("<b> Bye </b>");
         });




            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }

But when I run it , I get :

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware1 An unhandled exception has occurred while executing the request. System.InvalidOperationException: Headers are read-only, response has already started. With this HTML :

enter image description here

I've been searching for a solution in SO but didn't find, how to do it.

Question:

Why is it happening? I thought I can control the pipeline and do whatever I want it via calling next() on the pipeline.

How can I add my custom HTML tags before and after?

Edit:

If I move the code to the end of the Configure method, I see the regular output , without getting the exception, but without the HTML tags.

Edit #2 :

I've also tried with OnStarting event , but still , no success (I get an empty page):

 app.Use(async (context, next) =>
        {
          
            context.Response.OnStarting(async state =>
            {
                if (state is HttpContext httpContext)
                {
                    var request = httpContext.Request;
                    var response = httpContext.Response;
                    await response .WriteAsync("<b> Bye </b>"); // <----
               }
            }, context);
            await next();
             
        });
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • Have you done View Source? Are you sure the browser isn't hiding the output? – bfren May 16 '21 at 15:58
  • @bcg yes. I did it. it writes the first statment only. https://i.imgur.com/RG0Ktwb.jpg ( for [this](https://i.imgur.com/lnDmF9H.jpg) code – Royi Namir May 17 '21 at 14:13
  • @OP What's the content-type of the response body? – Peter Csala May 25 '21 at 13:28
  • @PeterCsala application/json. ( i know that adding HTML tags before and after is a mismatch. but I'm after pure text contamination. ( i dont mind changing appication/json to text/html at response time....) – Royi Namir May 25 '21 at 13:31
  • Okay, which version of ASP.NET Core are you using? – Peter Csala May 25 '21 at 13:32
  • 1
    @PeterCsala Can you please have a look at [this](https://stackoverflow.com/questions/67835490/polly-wont-throw-on-some-exceptions) ? – Royi Namir Jun 04 '21 at 10:16

2 Answers2

4

With the following middleware I was able to add html tags before and after the action result:

public class BeforeAfterMiddleware
{
    private readonly RequestDelegate _next;
    public BeforeAfterMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        //Redirect response into a memorystream
        using var actionResponse = new MemoryStream();
        var httpResponse = context.Response.Body;
        context.Response.Body = actionResponse;

        //Call the handler action
        await _next.Invoke(context);

        //Read the handler response from the memory stream
        actionResponse.Seek(0, SeekOrigin.Begin);
        var reader = new StreamReader(actionResponse);
        using var bufferReader = new StreamReader(actionResponse);
        string body = await bufferReader.ReadToEndAsync();

        //Remove the handler's response from the memory stream
        context.Response.Clear();

        //Write data to the memorystream
        await context.Response.WriteAsync("<h1>HI</h1>");
        await context.Response.WriteAsync(body);
        await context.Response.WriteAsync("<h1>BYE</h1>");

        //Copy memorystream to the response stream
        context.Response.Body.Seek(0, SeekOrigin.Begin);
        await context.Response.Body.CopyToAsync(httpResponse);
        context.Request.Body = httpResponse;
    }
}
  1. It simply redirects the response to a MemoryStream
  2. then alters that with some text before and after
  3. finally redirects memoryStream back to the response stream

Usage: app.UseMiddleware<BeforeAfterMiddleware>();

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 1
    Nicely done...and it is a class instead of inline... – cbuteau May 25 '21 at 16:23
  • 1
    You may remove response headers set elsewhere with the `context.Response.Clear()`, which sometimes isn't desirable. How about using a `using var writer = new StreamWriter(httpResponse)` and then `await writer.WriteAsync("

    HI

    ")` and so on? (If so, the ending part from "//Copy memorystream to the response stream" potentially could be removed.)
    – Jakob Bagterp May 27 '23 at 11:49
  • @JakobBagterp Yes, you are right that `Clear` resets not just the body but the headers and status code as well. Quite frankly I'm not sure I fully understand your comment's second half. Could you please post the suggested alternative to pastebin (or similar)? – Peter Csala May 30 '23 at 08:39
2

OK, I think I have it! It's extremely challenging as you've worked out... the way I've done it is by writing a custom IOutputFormatter.

// in ConfigureServices()
services.AddControllers(opt =>
{
    opt.OutputFormatters.Clear();
    opt.OutputFormatters.Add(new AppendHtmlOutputFormatter());
});

// Formatter class
public class AppendHtmlOutputFormatter : IOutputFormatter
{
    public bool CanWriteResult(OutputFormatterCanWriteContext context) =>
        true; // add some logic here if you don't want to append all the time

    public Task WriteAsync(OutputFormatterWriteContext context)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(context.Object);

        var modified = "<b>Hi!</b>" + json + "<b>Bye!</b>";
        return context.HttpContext.Response.WriteAsync(modified);
    }
}

Now when I run an API endpoint I get the following response:

<b>Hi!</b>{"Bar":42}<b>Bye!</b>

Is that what you're looking for?

Default Output Formatters

Be aware that the following default OutputFormatters are removed by .Clear() - in this order:

  1. HttpNoContentFormatter
  2. StringOutputFormatter
  3. StreamOutputFormatter
  4. SystemTextJsonOutputFormatter

The solution above replaces all these and uses AppendHtmlOutputFormatter for everything. Therefore the following may be a preferred option (though won't append the HTML output to everything):

// in ConfigureServices()
services.AddControllers(opt =>
{
    opt.OutputFormatters.Clear();
    opt.OutputFormatters.Add(new HttpNoContentOutputFormatter());
    opt.OutputFormatters.Add(new StreamOutputFormatter());
    opt.OutputFormatters.Add(new AppendHtmlOutputFormatter());
});

Alternative to .Clear()

If you don't remove the default formatters, .NET will use those and never reach the custom formatter. However, if you prefer not to remove all formatters (e.g. another feature is adding them in), you can also remove them one at a time by type:

services.AddControllers(opt =>
{
    opt.OutputFormatters.RemoveType<StringOutputFormatter>();
    opt.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
    opt.OutputFormatters.Add(new AppendHtmlOutputFormatter());
});
bfren
  • 473
  • 3
  • 7
  • yeah. but how can i know the fefaults formatters ? BTW I've managed to do it in some way ....https://i.imgur.com/mcBANLP.jpg....I did replace there but again , it's the same ( replace vs add). one thing to mention here is that content.length had to be recalculated.... – Royi Namir May 18 '21 at 08:20
  • I definitely suggest trying to find a solution that does not involved reading or modifying the Response Body - because .NET Core does everything it can to stop you doing that. – bfren May 18 '21 at 08:27
  • I've added an alternative to `Clear()` which allows removal of specific OutputFormatters which may be preferable. – bfren May 18 '21 at 08:30
  • I wonder ....why must converter be removed ? why can'y i just add ? – Royi Namir May 18 '21 at 08:31
  • If you just add, you'll find nothing changes because .NET uses `SystemTextJsonOutputFormatter` before it reaches the custom one. You could insert the custom formatter earlier in the list, but it makes more sense to me to remove it. – bfren May 18 '21 at 08:33