From a .NET Core controller, is it possible to perform a WriteAsync on HttpResponse but control where on the rendered content the subsequent WriteAsync calls will write? Ex. Flush 1 sends a complete HTML document (maybe the layout view) but all subsequent WriteAsync calls render in the HTML body rather than appending to the end.
I have a process that involves a small series of redirects and this question is related to one one stop in that process. Unfortunately some of the steps in this stop are fairly long running and I'd like to render step feedback to the client before sending them to the next stop. However, I'd like it to look nice (not just text streaming down the page) so being able to control where it renders in the document would be a nice touch.
I realize there are other ways to do this (SignalR etc.) but because this is only one stop the redirect series, I want to keep the request open until I'm ready to redirect to the next stop and was hoping I could keep the solution very simple.
After getting creative I found a way around my question more than a solution to do exactly what I asked for. After I discovered that JavaScript <script>
blocks will still be processed by the browser even if they are inserted to the DOM following the closing </html>
tag, I realized that I could leverage immediate functions to add elements where I wanted them to appear in the body. My solution does the following:
EXAMPLE
Controller .cs file:
[Route("example")]
public class ExampleController : Controller
{
private readonly ILogger<ExampleController> _logger;
private readonly IExampleControllerBL _controllerBl;
private const string htmlPage = @"
<!DOCTYPE HTML>
<html>
<head>
<title>Learning Management - Course Auto-Enrollment</title>
<style>
body {
font-size: 1.15em;
color: #333;
font-family: Arial, Helvetica;
height:100%;
}
.stepRow {
display: flex;
width: 100% ;
}
.stepCell, {
display: flex;
height: 2rem;
align-items: center;
}
#output {
min-height: 350px;
padding-bottom: 15px 0;
}
#footer {
background-color: #ccc;
padding: 20px;
margin: 0;
}
</style>
<script>
function addStep(stepText)
{
var output = document.querySelector('#output');
var stepRow = document.createElement('div');
var stepCell = document.createElement('div');
stepRow.className = 'stepRow';
stepCell.className = 'stepCell';
stepRow.appendChild(stepCell);
output.appendChild(stepRow);
stepCell.innerHTML = stepText;
}
</script>
</head>
<body>
<h1>Flushed Response Example</h1>
<p>Example of flushing output back to the client and then redirecting when complete.</p>
<h3>Progress:</h3>
<div id=""output""></div>
<div id=""footer"">Footer here</div>
</body>
</html>
";
private const string stepTemplate = @"
<script>
(function(){{
addStep('• {0}');
}}());
</script>
";
private const string redirectTemplate = @"
<script>
(function(){{
window.location.replace('{0}');
}}());
</script>
";
public ExampleController(ILogger<ExampleController> logger, IExampleControllerBL controllerBl)
{
_logger = logger;
_controllerBl = controllerBl;
}
[HttpGet]
[Route("steps"), HttpGet]
public async Task SteppedResponseExample(CancellationToken cancellationToken)
{
//Write full HTML page back to response
Response.Clear();
await Response.WriteAsync(htmlPage, cancellationToken);
//Execute business logic and pass step change handler
await _controllerBl.ExecuteMultiStepProcess(async (step) =>
{
//Write step script block back to response
await Response.WriteAsync(string.Format(stepTemplate, step), cancellationToken);
});
//Write redirect script block to response
await Response.WriteAsync(string.Format(redirectTemplate, "https://stackoverflow.com/questions/57330559/asp-net-core-controlling-httpresponse-flush-output-location"), cancellationToken);
}
}
Business logic .cs file:
public class ExampleControllerBL : IExampleControllerBL
{
public async Task ExecuteMultiStepProcess(Func<string, Task> OnStartStep)
{
await OnStartStep("Performing step 1...");
await Task.Delay(2500);
await OnStartStep("Performing step 2...");
await Task.Delay(2500);
await OnStartStep("Performing step 3...");
await Task.Delay(2500);
await OnStartStep("Performing step 4...");
await Task.Delay(2500);
await OnStartStep("Performing step 5...");
await Task.Delay(2500);
await OnStartStep("Performing step 6...");
await Task.Delay(2500);
}
}
Once you begin flushing the response, trying to return an IAction
result object is pointless because the MVC middleware pipeline will throw errors because the status code is already set etc. by flushing. That is the reason my endpoint doesn't return anything.
Clearly error handling needs to be considered because it also has to be handled through flushing unless you just want to leave the end user confused about why it just stopped. However, this is a nice way to keep an end user informed about a process that involves redirects and processing. Hope this is useful for someone else.
EDIT: I did discover that in this use case there actually isn't a need to "Flush" the response body.