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 :
Just for testing purpose, I want to add HTML tag before and after the response :
Something like ( edited manually in DOM ) :
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 :
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();
});
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;
}
}
MemoryStream
Usage: app.UseMiddleware<BeforeAfterMiddleware>();
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?
Be aware that the following default OutputFormatters are removed by .Clear()
- in this order:
HttpNoContentFormatter
StringOutputFormatter
StreamOutputFormatter
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());
});
.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());
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With