I have created an HttpGet
in my Server-API which creates a CSV-File and returns it with FileStreamResult
:
[HttpGet]
public IActionResult Get() {
// do logic to create csv in memoryStream
return new FileStreamResult(memoryStream, "text/csv;charset=utf-8") {
FileDownloadName = "products.csv",
};
}
In my Blazor-Client App, I have created a Button with a handler:
private async Task DownloadCatalog() {
var file = HttpClient.GetAsync("api/csvProduct");
// ... how do I download the file in the browser?
}
The Get in the Controller is called, but I don't know what to do so that the file is downloaded in the browser after the api call.
Blazor code can now browse local file systems and open up local files and edit them in native Windows apps.
The Blazor Server hosting model offers several benefits: Download size is significantly smaller than a Blazor WebAssembly app, and the app loads much faster. -The app takes full advantage of server capabilities, including the use of . NET Core APIs.
Browsers don't allow scripts to write to the file system, whether written in JavaScript or WebAssembly. The download dialog is displayed by the browser only when the user clicks on a link.
Using a link button
If the final file is returned directly from the server, the easiest solution is to use a link button with a URL to the API endpoint, possibly calculated at runtime. You can use the download
attribute to specify a file name. When the user clicks on the link, the file will be retrieved and saved using the download
name
For example :
<a id="exportCsv" class="btn" href="api/csvProduct" download="MyFile.csv"
role="button" target="=_top">Export to CSV</a>
or
@if (_exportUrl != null)
{
<a id="exportCsv" class="btn" href="@_exportUrl" download="MyFile.csv"
role="button" target="=_top">Export to Csv</a>
}
...
int _productId=0;
string? _exportUrl=null;
async Task Search()
{
//Get and display a product summary
_model=await GetProductSummary(_productId);
//Activate the download URL
_exportUrl = $"api/csvProduct/{_productId}";
}
Using a dynamically generated data link
If that's not possible, you have to create a link element in JavaScript with a data URL, or a Blob, and click it. That's SLOOOOW for three reasons :
The article Generating and efficiently exporting a file in a Blazor WebAssembly application shows how to pass the bytes without marshaling using some Blazor runtime tricks.
If you use Blazor WASM, you can use use InvokeUnmarshalled
to pass a byte[]
array and have it appear as a Uint8Array
in JavaScript.
byte[] file = Enumerable.Range(0, 100).Cast<byte>().ToArray();
string fileName = "file.bin";
string contentType = "application/octet-stream";
// Check if the IJSRuntime is the WebAssembly implementation of the JSRuntime
if (JSRuntime is IJSUnmarshalledRuntime webAssemblyJSRuntime)
{
webAssemblyJSRuntime.InvokeUnmarshalled<string, string, byte[], bool>("BlazorDownloadFileFast", fileName, contentType, file);
}
else
{
// Fall back to the slow method if not in WebAssembly
await JSRuntime.InvokeVoidAsync("BlazorDownloadFile", fileName, contentType, file);
}
The BlazorDownloadFileFast
JavaScript method retrieves the array, converts it to a File and then, through URL.createObjectURL
to a safe data URL that can be clicked :
function BlazorDownloadFileFast(name, contentType, content) {
// Convert the parameters to actual JS types
const nameStr = BINDING.conv_string(name);
const contentTypeStr = BINDING.conv_string(contentType);
const contentArray = Blazor.platform.toUint8Array(content);
// Create the URL
const file = new File([contentArray], nameStr, { type: contentTypeStr });
const exportUrl = URL.createObjectURL(file);
// Create the <a> element and click on it
const a = document.createElement("a");
document.body.appendChild(a);
a.href = exportUrl;
a.download = nameStr;
a.target = "_self";
a.click();
// We don't need to keep the url, let's release the memory
// On Safari it seems you need to comment this line... (please let me know if you know why)
URL.revokeObjectURL(exportUrl);
a.remove();
}
With Blazor Server, marshaling is unavoidable. In this case the slower BlazorDownloadFile
method is called. The byte[]
array is marshaled as a BASE64 string which has to be decoded. Unfortunately, JavaScript's atob
and btoa
functions can't handle every value so we need another method to decode Base64 into Uint8Array:
function BlazorDownloadFile(filename, contentType, content) {
// Blazor marshall byte[] to a base64 string, so we first need to convert the string (content) to a Uint8Array to create the File
const data = base64DecToArr(content);
// Create the URL
const file = new File([data], filename, { type: contentType });
const exportUrl = URL.createObjectURL(file);
// Create the <a> element and click on it
const a = document.createElement("a");
document.body.appendChild(a);
a.href = exportUrl;
a.download = filename;
a.target = "_self";
a.click();
// We don't need to keep the url, let's release the memory
// On Safari it seems you need to comment this line... (please let me know if you know why)
URL.revokeObjectURL(exportUrl);
a.remove();
}
And the decoder function, borrowed from Mozilla's Base64 documentation
// Convert a base64 string to a Uint8Array. This is needed to create a blob object from the base64 string.
// The code comes from: https://developer.mozilla.org/fr/docs/Web/API/WindowBase64/D%C3%A9coder_encoder_en_base64
function b64ToUint6(nChr) {
return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0;
}
function base64DecToArr(sBase64, nBlocksSize) {
var
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""),
nInLen = sB64Enc.length,
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2,
taBytes = new Uint8Array(nOutLen);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
}
nUint24 = 0;
}
}
return taBytes;
}
Blazor 6
The ASP.NET Core 6 Preview 6 that was released recently no longer marshals byte[]
as a Base64 string. It should be possible to use the following function
function BlazorDownloadFile(filename, contentType, data) {
// Create the URL
const file = new File([data], filename, { type: contentType });
const exportUrl = URL.createObjectURL(file);
// Create the <a> element and click on it
const a = document.createElement("a");
document.body.appendChild(a);
a.href = exportUrl;
a.download = filename;
a.target = "_self";
a.click();
// We don't need to keep the url, let's release the memory
// On Safari it seems you need to comment this line... (please let me know if you know why)
URL.revokeObjectURL(exportUrl);
a.remove();
}
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