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:
- The MVC Controller writes a full HTML page to the response and
flushes it back to the client
- As subsequent steps are processed, the controller writes
blocks of script to the response and flushes them
- These script blocks are immediate functions that manipulate the DOM components to add the steps I want to display before actually terminating the request
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.