Tag Archives: WebAssembly

WASM: AggregateException_ctor_DefaultMessage (Could not resolve type with token …)

Narazili jsme po instalaci .NET 9 SDK 9.0.204 (a nepomohl ani 9.0.300) na zajímavou chybu published Blazor WebAssembly front-endů (browser console výstup, front-end nenabíhá):

ManagedError: AggregateException_ctor_DefaultMessage (Could not resolve type with token 01000024 from typeref (expected class 'System.Reflection.Assembly' in assembly 'netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'))
    at an (dotnet.runtime.5nhp1wfg9b.js:3:26894)
    at Kt.resolve_or_reject (dotnet.runtime.5nhp1wfg9b.js:3:26449)
    at dotnet.runtime.5nhp1wfg9b.js:3:172714
    at dotnet.runtime.5nhp1wfg9b.js:3:172778
    at fr (dotnet.runtime.5nhp1wfg9b.js:3:35046)
    at Fc (dotnet.runtime.5nhp1wfg9b.js:3:172361)
    at dotnet.native.swgexbmoy7.wasm:0x1f1a4
    at dotnet.native.swgexbmoy7.wasm:0x1c8ae
    at dotnet.native.swgexbmoy7.wasm:0xea19
    at dotnet.native.swgexbmoy7.wasm:0x1ec88

První podezření bylo na trimming, nicméně když to zkrátím, tak se ukázalo, že se jedná o klasický problém buildů po instalaci nového SDK – je potřeba vymazat pracovní složky build-agentů, pokud každý váš build neběží na úplně čistém prostředí, ale používáte nějakou formu inkrementálního uspořádání. Když to převedu do roviny lokálního vývoje s Visual Studiem, je potřeba udělat Clean solution a vymazat složky bin a obj.

Proč to vůbec píšu? Kdyby někoho potkala stejná chyba, při troše štěstí vygooglí tento post a ušetří si čas s diagnostikou. U nás už jsme si poměrně zvykli, že když po instalaci nové verze SDK padá build, je potřeba před dalším bádáním vymazat pracovní složky build-agentů. Poprvé v historii se nám však stalo, že build úspěšně prošel (nepadal), ale výsledek build byl „vadný“ způsobem, který se projevil až při spuštění Blazor WASM front-endu v browseru.

Blazor NET8 – Životní cyklus komponent [Jiří Kanda, HAVIT Vzdělávací okénko, 28.3.2024]

Záznam ze Vzdělávacího okénka HAVIT z 28. března 2024, kde Jiří Kanda povídal o Blazoru – o životním cyklu komponent a jeho zákoutích – aktualizovaná podoba session z roku 2019.

Blazor – Komponenty [Jiří Kanda, HAVIT Vzdělávací okénko, 13.3.2024]

Záznam ze Vzdělávacího okénka HAVIT z 13. března 2024, kde Jiří Kanda povídal o Blazoru – o fungování jeho komponent – aktualizovaná podoba session z roku 2019.

Blazor – Úvod, hosting, modely [Jiří Kanda, HAVIT Vzdělávací okénko, 6.3.2024]

Záznam ze Vzdělávacího okénka HAVIT z 6. března 2024, kde Jiří Kanda povídal o Blazoru – jednalo se o úvod do hostingových modelů – aktualizovaná podoba session z roku 2019.

Blazor novinky v .NET 8 + Blazor Performance Tuning – záznam a slides [Robert Haken, WUG Dev Day, 4.2.2024]

Záznam ze Vzdělávacího okénka HAVIT z 8. listopadu 2023, kde jsem telegraficky představoval novinky přicházející v „.NET 8 vlně“.

Slides

gRPC code-first pro Blazor WebAssembly front-end

gRPC je fenomén dnešní doby. Moderní a výkonově efektivní protokol se rychle rozšiřuje a my si dnes ukážeme, jak ho použít pro komunikaci mez Blazor WebAssembly front-endem a ASP.NET Core backendem (hostem):

  1. Efektivně využijeme možnosti sdílení kódu mezi serverovou a klientskou částí. Použijeme uspořádání code-first a „contract“ (interface pro volanou službu a definice datových objektů) dáme do assembly sdílené serverovou i klientskou částí solution.
  2. Pro překonání omezení browserů použijeme gRPC-Web rozšíření.

Celou implementaci si ukážeme na jednoduché příkladu – použijeme výchozí šablonu Blazor WebAssembly App z Visual Studia (ASP.NET Core hosted, verze šablony .NET7) a připravenou ukázku Fetch data, která v této šabloně používá REST API, předěláme na gRPC-Web volání s použitím code-first.

Pojďme na to, bude to jen pár kroků:

1. MyBlazorSolution.Server – Příprava ASP.NET Core host

Nejprve si na straně serveru připravíme infrastrukturu pro gRPC. Půjdeme rovnou do varianty s rozšířením gRPC-Web s code-first podporou a nainstalujeme NuGet balíčky

Zaregistrujeme podpůrné služby do dependency-injection ve Startup.cs:

builder.Services.AddCodeFirstGrpc(config => { config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal; });

Přidáme gRPC middleware někam mezi UseRouting() a definici end-pointů (před MapXy() metody):

app.UseGrpcWeb(new GrpcWebOptions() { DefaultEnabled = true });

2. MyBlazorSolution.Shared – Definice contractu služeb (code-first)

Nyní v podobě interface definujeme, jak bude naše služba vypadat. Interface následně využijeme jak na straně serveru (vytvoříme jeho implementaci), tak na straně klienta (necháme si vygenerovat gRPC-klienta, který bude interface implementovat a my ho v našem kódu budeme přímo využívat prostřednictvím dependency injection).

Přidejte do projektu NuGet balíček, který nám umožní interface dekorovat potřebnými atributy

Najděte si ukázkový soubor WeatherForecast.cs z šablony projekty. Obsahuje definici návratové datové zprávy, kterou nám nyní vrací ukázkové REST API. Tuto třídu předěláme do následující podoby:

[DataContract]
public class WeatherForecast
{
	[DataMember(Order = 1)]
	public DateTime Date { get; set; }

	[DataMember(Order = 2)]
	public int TemperatureC { get; set; }

	[DataMember(Order = 3)]
	public string? Summary { get; set; }

	public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
  • Přidali jsme atribut DataContract pro označení třídy, kterou budeme používat jako datovou zprávu gRPC.
  • Přidali jsme atributy DataMember, kterými jsme označili prvky, které se mají přes gRPC přenášet (ostatní se ignorují, zde TemperatureF je počítané a dopočítá se nám z ostatních údajů na klientovi kdykoliv znovu). Každému prvku je potřeba nastavit Order, kterým se definuje pevný layout pro používanou protobuf serializaci.
  • Původní typ DateOnly jsme vyměnili za DateTime. Musíme se držet typů podporovaných použitou protobuf serializací.

Dále potřebujeme vytvořit interface, který bude celou službu popisovat:

[ServiceContract]
public interface IWeatherForecastFacade
{
	Task<List<WeatherForecast>> GetForecastAsync(CancellationToken cancellationToken = default);
}
  • Atribut [ServiceContract] nám označuje použitelnost pro gRPC (lze později využít i pro automatické registrace).
  • Z podstaty síťové komunikace by měl být celý interface asynchronní.
  • Můžeme použít volitelný CancellationToken, který nám může zprostředkovat signál o předčasném ukončení komunikace klientem (nebo přerušení spojení).

3. MyBlazorSolution.Server – Implementace a publikování služby

Nyní zbývá připravený interface implementovat na straně serveru (použijeme lehce modifikovaný kód z ukázkového WeatherForecastController, který můžete nyní smazat):

public class WeatherForecastFacade : IWeatherForecastFacade
{
	private static readonly string[] Summaries = new[]
	{
		"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
	};

	public Task<List<WeatherForecast>> GetForecastAsync(CancellationToken cancellationToken = default)
	{
		return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
		{
			Date = DateTime.Today.AddDays(index),
			TemperatureC = Random.Shared.Next(-20, 55),
			Summary = Summaries[Random.Shared.Next(Summaries.Length)]
		})
		.ToList());
	}
}

Připravenou službu vypublikujeme skrze Startup.cs:

app.MapGrpcService<WeatherForecastFacade>();

MyBlazorSolution.Client – konzumace gRPC služby

Nyní už zbývá jen službu použít v Blazor WebAssembly front-endu. Celou definici máme k dispozici v podobě IWeatherForecastFacade interface s jeho WeatherForecast datovou třídou.

Do projektu přidáme potřebné NuGet balíčky:

V Program.cs zaregistrujeme gRPC-Web infrastrukturu a klienta (ve factory podobě):

builder.Services.AddTransient<GrpcWebHandler>(provider => new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));

builder.Services.AddCodeFirstGrpcClient<IWeatherForecastFacade>((provider, options) =>
	{
		var navigationManager = provider.GetRequiredService<NavigationManager>();
		var backendUrl = navigationManager.BaseUri;

		options.Address = new Uri(backendUrl);
	})
	.ConfigurePrimaryHttpMessageHandler<GrpcWebHandler>();

No a nyní již můžeme IWeatherForecastFacade ve front-endovém projektu kdekoliv použít tak, že si necháme službu nainjectovat pomocí dependency injection. Například tedy upravíme FetchData.razor, aby používalo naší novou gRPC službu namísto původního REST API:

@inject IWeatherForecastFacade WeatherForecastFacade

...

@code {
	private List<WeatherForecast>? forecasts;

	protected override async Task OnInitializedAsync()
	{
		forecasts = await WeatherForecastFacade.GetForecastAsync();
	}
}

Hotovo. Projekt by měl být nyní spustitelný a stránka Fetch data bude nyní komunikovat prostřednictvím gRPC-Web.

Svoje řešení si můžete ověřit vůči vzorové repository

Další rozšíření a pokročilejší techniky

gRPC služba může samozřejmě přijímat i vstup. V takovém případě použijte jeden vstupní parametr pro příchozí zprávu – datovou třídu vytvořenou stejně jako jsme připravili výstupní WeatherForecast. Obvykle se tyto třídy označují jako Data Transfer Object a dostávají tak suffix Dto. Implementace je obvykle jako C# record.

Pokud máme v našem projektu authentizaci a authorizaci, pak můžeme na implementující třídě/metodě použít atribut [Authorize], stejně jako bychom ho použili na controlleru/action.

Na publikovaný gRPC endpoint můžeme aplikovat libovolné techniky jako na jakýkoliv jiný namapovaný serverový endpoint (rate limiting, caching, …).

gRPC má v sobě podporu interceptorů jejichž prostřednictvím můžeme gRPC komunikaci dále vylepšovat

  • předávat si ze serveru na klienta výjimky (základní podpora je vestavěna, ale může se vám hodit ji obohatit o specifický handling vlastních scénářů),
  • předávat si z klienta na server požadovanou culture (do jakého jazyka je front-end přepnut),

V pokročilejší variantě uspořádání můžete zajistit i automatickou registraci interface a datových contractů, aniž byste je museli dekorovat atributy [ServiceContract], [DataContract] a [DataMember(Order = ...)]. Vše toto a mnohé další najdete připravené v:

Obojí open-source s MIT licencí, zdarma.

HxInputFile – rozšíření Blazor InputFile komponenty o přímý (nativní) upload a progress-indikaci

V .NET 5. dostal Blazor InputFile komponentu, která zpřístupňuje vybrané soubory v podobě streamu (IBrowserFile.OpenReadStream).

V případě Blazor Serveru je to celkem použitelné, implementace InputFile protlačí obsah souboru na server pomocí SignalR téměř plnou rychlostí (odhadem o 30% pomaleji než nativní HTTP upload). V případě Blazor WebAssembly je ale implementace přenosu dat na server ponechána na vývojářích a možnosti nejsou růžové.

Blazor dokáže přes unmarshalled interop dostat obsah souboru do WASM velmi rychle (cca 100MB/s), jenže vyšťouchat větší soubor z WASM na server je o dost horší a pro větší soubory nepoužitelné:

Přímý (nativní) upload

Abychom překonali limity InputFile komponenty ve WASM, můžeme soubory uploadovat na server přímo z JavaScriptu pomocí XMLHttpRequest (fetch() API se na to moc nehodí, protože zatím nedává rozumné informace o progressu):

var data = new FormData();
data.append('file', file, file.name);
 
var request = new XMLHttpRequest();
request.open('POST', uploadEndpointUrl, true);
request.send(data);

Progress indikace

XMLHttpRequest nám dává hezké API v podobě události onprogress:

request.upload.onprogress = function (e) {
    // e.loaded - bytes already uploaded
    // e.total - total upload size (slightly bigger than the file size)
};

HxInputFile / HxInputFileCore

Když to dáme celé dohromady, můžeme vytvořit komponentu zděděnou z InputFile. Říkejme jí HxInputFileCore (a k tomu HxInputFile, který už je Bootstrap podobou s drobnými vylepšeními kolem).

Klíčové části zdrojového kódu komponenty pak budou:

public partial class HxInputFileCore : InputFile, IAsyncDisposable
{
    [Parameter] public string UploadUrl { get; set; }
    [Parameter] public EventCallback<UploadProgressEventArgs> OnProgress { get; set; }
    [Parameter] public EventCallback<FileUploadedEventArgs> OnFileUploaded { get; set; }
    [Parameter] public EventCallback<UploadCompletedEventArgs> OnUploadCompleted { get; set; }
    [Parameter] public bool Multiple { get; set; }
    [Parameter] public string Id { get; set; } = "hx" + Guid.NewGuid().ToString("N");
 
    [Inject] protected IJSRuntime JSRuntime { get; set; }
 
    private DotNetObjectReference<HxInputFileCore> dotnetObjectReference;
    private IJSObjectReference jsModule;
 
    public HxInputFileCore()
    {
        dotnetObjectReference = DotNetObjectReference.Create(this);
    }
 
    protected override void OnParametersSet()
    {
        base.OnParametersSet();
 
        // TODO Temporary hack as base implementation of InputFile does not expose ElementReference (vNext: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/InputFile.cs)
        AdditionalAttributes ??= new Dictionary<string, object>();
        AdditionalAttributes["id"] = this.Id;
        AdditionalAttributes["multiple"] = this.Multiple;
    }
 
    public async Task StartUploadAsync(string accessToken = null)
    {
        jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/Havit.Blazor.Components.Web/hxinputfilecore.js");
        await jsModule.InvokeVoidAsync("upload", Id, dotnetObjectReference, this.UploadUrl, accessToken);
    }
 
    [JSInvokable("HxInputFileCore_HandleUploadProgress")]
    public async Task HandleUploadProgress(int fileIndex, string fileName, long loaded, long total)
    {
        var uploadProgress = new UploadProgressEventArgs() { /*...*/    };
        await OnProgress.InvokeAsync(uploadProgress);
    }
 
    [JSInvokable("HxInputFileCore_HandleFileUploaded")]
    public async Task HandleFileUploaded(int fileIndex, string fileName, long fileSize, string fileType, long fileLastModified, int responseStatus, string responseText)
    {
        var fileUploaded = new FileUploadedEventArgs() { /* ... */ };
        await OnFileUploaded.InvokeAsync(fileUploaded);
    }
 
    [JSInvokable("HxInputFileCore_HandleUploadCompleted")]
    public async Task HandleUploadCompleted(int fileCount, long totalSize)
    {
        var uploadCompleted = new UploadCompletedEventArgs() { /* ... */
        };
        await OnUploadCompleted.InvokeAsync(uploadCompleted);
    }
 
    public async ValueTask DisposeAsync()
    {
        // ...
    }
}

…a podpůrný JavaScript k tomu:

export function upload(inputElementId, hxInputFileDotnetObjectReference, uploadEndpointUrl, accessToken) {
    var inputElement = document.getElementById(inputElementId);
    var dotnetReference = hxInputFileDotnetObjectReference;
    var files = inputElement.files;
    var totalSize = 0;
    var uploadedCounter = 0;
 
    for (var i = 0; i < files.length; i++) {
        (function (curr) {
            var index = curr;
            var file = files[curr];
            totalSize = totalSize + file.size;
 
            var data = new FormData();
            data.append('file', file, file.name);
 
            var request = new XMLHttpRequest();
            request.open('POST', uploadEndpointUrl, true);
 
            if (accessToken) {
                request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
            }
 
            request.upload.onprogress = function (e) {
                dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadProgress', index, file.name, e.loaded, e.total);
            };
            request.onreadystatechange = function () {
                if (request.readyState === 4) {
                    dotnetReference.invokeMethodAsync('HxInputFileCore_HandleFileUploaded', index, file.name, file.size, file.type, file.lastModified, request.status, request.responseText);
                };
 
                uploadedCounter++;
                if (uploadedCounter === files.length) {
                    dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadCompleted', files.length, totalSize);
                }
            }
 
            request.send(data);
        }(i));
    }
}

Komponenty jsou součástí open-source balíčku Havit.Blazor, který najdete na GitHub.

Použití

<HxInputFile @ref="hxInputFileComponent" Label="HxInputFile" UploadUrl="/file-upload-streamed/" OnProgress="HandleProgress" OnFileUploaded="HandleFileUploaded" OnUploadCompleted="HandleUploadCompleted" Multiple="true" />
 
<HxButton Text="Upload" OnClick="HandleUploadClick" />
 
@code
{
    private HxInputFile hxInputFileComponent;
 
    private async Task HandleUploadClick()
    {
        files.Clear();
 
        string accessToken = null;
         
        var accessTokenResult = await ... // use IAccessTokenProvider
        await hxInputFileComponent.StartUploadAsync(accessToken);
    }
 
    private Task HandleProgress(UploadProgressEventArgs progress)
    {
        // indicate progress here
    }
 
    private Task HandleFileUploaded(FileUploadedEventArgs fileUploaded)
    {
        // individual file uploaded
    }
 
    private Task HandleUploadCompleted(UploadCompletedEventArgs uploadCompleted)
    {
        // all files uploaded
    }
}

TODOs

První verze komponenty neřeší některé detaily. Bude potřeba ještě:

  • Limit velikosti souborů
  • Limit počtu paralelně uploadovaných souborů
  • Vylepšit obsluhu chyb
  • …?

Odkazy

Viz též

Líbí se Vám Blazor?

Přidejte se k nám…

Blazor – Stav světa a co se chystá v .NET5 – záznam, slides, dema [WUG Days Online 2020]

Záznam z přednášky pro konferenci WUG Days Online 2020, kterou pořádalo sdružení WUG Česká Republika ve dnech 21. až 23. října 2020.

DEMA: https://github.com/hakenr/BlazorNet5Demos

HAVIT & Blazor – pohled do kuchyně [Jiří Kanda, Vzdělávací okénko, 29.7.2020]

Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval, jaký stack máme v HAVITu připravený pro vývoj aplikací v Blazor.

Blazor WebAssembly + gRPC-Web code-first [Robert Haken, Vzdělávací okénko, 15.4.2020]

Záznam ze Vzdělávacího okénka HAVIT z 15. dubna 2020, kde jsem povídal o komunikaci mezi Blazor WebAssembly front-endem a ASP.NET Core backendem prostřednictvím gRPC-Web protokolu a s code-first přístupem (bez .proto souborů).

Nahrávka je publikována na našem HAVIT YouTube Channelu.