I did it thus:
Added a new DownloadController.cs to a folder called Controllers
[Controller, Microsoft.AspNetCore.Mvc.Route("/[controller]/[action]")]
public class DownloadController : Controller
{
private readonly IDataCombinerService DataCombinerService;
private readonly IDataLocatorService DataLocatorService;
public DownloadController(IDataCombinerService dataCombinerService, IDataLocatorService dataLocatorService)
{
DataCombinerService = dataCombinerService;
DataLocatorService = dataLocatorService;
}
[HttpGet]
[ActionName("Accounts")]
public async Task<IActionResult> Accounts()
{
var cts = new CancellationTokenSource();
var Accounts = await DataCombinerService.CombineAccounts(await DataLocatorService.GetDataLocationsAsync(cts.Token), cts.Token);
var json = JsonSerializer.SerializeToUtf8Bytes(Accounts, Accounts.GetType(), new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
var stream = new MemoryStream(json);
var fResult = new FileStreamResult(stream, MediaTypeNames.Application.Json)
{
FileDownloadName = $"Account Export {DateTime.Now.ToString("yyyyMMdd")}.json"
};
return fResult;
}
[HttpGet]
public IActionResult Index()
{
return View();
}
}
Strictly speaking async isn't required here as it doesn't need to process anything else, but that method is used to display the same results on screen when it is.
Then inside Startup.cs
app.UseEndpoints(endpoints =>
add:
endpoints.MapControllerRoute(
name: "default",
defaults: new { action = "Index" },
pattern: "{controller}/{action}");
endpoints.MapControllers();
Again the defaults isn't strictly speaking required, it's a standard MVC Controller.
This then functions just like a classic MVC response, so you can send back any files, from any source you like. It may be helpful to have a middleware service to hold temporary data between the view and the downloader controller so the client is downloading the same data.