How Can One Generate and Save a File Client Side Using Blazor

How can one generate and save a file client side using Blazor?

  1. Add a link

<a class="form-control btn btn-primary" href="/download?name=test.txt" target="_blank">Download</a>

How to create a new file on the server with blazor client-side application?

This message: "WASM: File created." indicates that you're using a Blazor WebAssembly App, right? The browser is not supposed to allow you to create a file on the client. This is only a message produced by your code.

Understand this: When you hit the button a normal post back occurs because the type of the button is 'submit'. After the return to the calling page, I guess, a click event is triggered (not sure but it may be so), and the code in the run method is executed to display the message "WASM: File created."

Understand this: You've created a Blazor WebAssembly App server hosted. This is a Single Page Application, and it should communicate with your server via a Web Api or SignleR. You must not use post backs, and you need to learn how the two modes of executions of Blazor work.

Download File from Server with Blazor App

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 :

  1. You're making an in-memory copy of the downloaded file that's at least 33% larger than the original.
  2. JS interop data marshalling is slow, which means that passing the bytes from Blazor to Javascript is also slow.
  3. Byte arrays are passed as Base64 strings. These need to be decoded back into a byte array to be used as blobs.

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();
}

Secure File Download Using Blazor Webassembly and ASP.NET Core

To secure a file download I use a one time token sent in the download request URI:

  1. Define a class to store one time toke
public class OneTimeToken
{
public string Id { get; set; }

public string ClientId { get; set; }

public string UserId { get; set; }

public string Data { get; set; }
}

I prefer to store tokens in DB but you can choose to store it in memory but server side obviously.


  1. Before download create a token

Here I use a service calling an API to create my token

public class OneTimeTokenService
{
private readonly IAdminStore<OneTimeToken> _store; // this my service calling the API
private readonly AuthenticationStateProvider _stateProvider;
private readonly IAccessTokenProvider _provider;
private readonly IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> _options;

public OneTimeTokenService(IAdminStore<OneTimeToken> store,
AuthenticationStateProvider state,
IAccessTokenProvider provider,
IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_stateProvider = state ?? throw new ArgumentNullException(nameof(state));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_options = options ?? throw new ArgumentNullException(nameof(options));
}

public async Task<string> GetOneTimeToken()
{
// gets the user access token
var tokenResult = await _provider.RequestAccessToken().ConfigureAwait(false);
tokenResult.TryGetToken(out AccessToken token);
// gets the authentication state
var state = await _stateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
// creates a one time token
var oneTimeToken = await _store.CreateAsync(new OneTimeToken
{
ClientId = _options.Value.ProviderOptions.ClientId,
UserId = state.User.Claims.First(c => c.Type == "sub").Value,
Expiration = DateTime.UtcNow.AddMinutes(1),
Data = token.Value
}).ConfigureAwait(false);

return oneTimeToken.Id;
}
}

  1. Create the download uri when the user click the download link

Here I use a button, but it work with a any html element, you can use a link instead.

@inject OneTimeTokenService _service
<button class="btn btn-secondary" @onclick="Download" >
<span class="oi oi-arrow-circle-top"></span><span class="sr-only">Download
</span>
</button>
@code {
private async Task Download()
{
var token = await _service.GetOneTimeToken().ConfigureAwait(false);
var url = $"http://locahost/stuff?otk={token}";
await _jsRuntime.InvokeVoidAsync("open", url, "_blank").ConfigureAwait(false);
}
}

  1. Retrieve the token from the URL

4.1. Add the package IdentityServer4.AccessTokenValidation to your API project.

In Startup ConfigureServices method use the IdentityServer authentication:

services.AddTransient<OneTimeTokenService>()
.AddAuthentication()
.AddIdentityServerAuthentication(options =>
{
options.TokenRetriever = request =>
{
var oneTimeToken = TokenRetrieval.FromQueryString("otk")(request);
if (!string.IsNullOrEmpty(oneTimeToken))
{
return request.HttpContext
.RequestServices
.GetRequiredService<OneTimeTokenService>()
.GetOneTimeToken(oneTimeToken);
}
return TokenRetrieval.FromAuthorizationHeader()(request);
};
});

  1. Define a service to read and consume the one time token from the URI

The token must not be reusable, so it's delete on each request.

Here it's just a sample. If you store tokens in DB you can use an EF context, if it's in memory, you can use an object cache for exemple.

public class OneTimeTokenService{
private readonly IAdminStore<OneTimeToken> _store;

public OneTimeTokenService(IAdminStore<OneTimeToken> store)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
}

public string GetOneTimeToken(string id)
{
// gets the token.
var token = _store.GetAsync(id, new GetRequest()).GetAwaiter().GetResult();
if (token == null)
{
return null;
}
// deletes the token to not reuse it.
_store.DeleteAsync(id).GetAwaiter().GetResult();
return token.Data;
}
}

blazor webassembly download file from api

The simplest way is to let the browser handle it:

<a class="btn btn-primary" href="@(Http.BaseAddress)YourController/Test1">download</a>

You get the base address from an inject HttpClient and make sure the URL matches the route you configured in the Controller.



Related Topics



Leave a reply



Submit