Bagikan melalui


Menangani kesalahan di aplikasi ASP.NET Core Blazor

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Artikel ini menjelaskan cara Blazor mengelola pengecualian yang tidak tertangani dan cara mengembangkan aplikasi yang mendeteksi dan menangani kesalahan.

Kesalahan terperinci selama pengembangan

Blazor Saat aplikasi tidak berfungsi dengan baik selama pengembangan, menerima informasi kesalahan terperinci dari aplikasi membantu pemecahan masalah dan memperbaiki masalah. Saat terjadi kesalahan, Blazor aplikasi menampilkan bilah kuning muda di bagian bawah layar:

  • Selama pengembangan, bilah mengarahkan Anda ke konsol browser, tempat Anda dapat melihat pengecualian.
  • Dalam produksi, bilah memberi tahu pengguna bahwa kesalahan telah terjadi dan merekomendasikan penyegaran browser.

Antarmuka pengguna untuk pengalaman penanganan kesalahan ini adalah bagian Blazor dari templat proyek. Tidak semua versi Blazor templat proyek menggunakan data-nosnippet atribut untuk memberi sinyal ke browser untuk tidak menyimpan konten antarmuka pengguna kesalahan, tetapi semua versi Blazor dokumentasi menerapkan atribut .

Blazor Di Aplikasi Web, kustomisasi pengalaman dalam MainLayout komponen. Karena Pembantu Tag Lingkungan (misalnya, <environment include="Production">...</environment>) tidak didukung dalam Razor komponen, contoh berikut menyuntikkan IHostEnvironment untuk mengonfigurasi pesan kesalahan untuk lingkungan yang berbeda.

Di bagian MainLayout.razoratas :

@inject IHostEnvironment HostEnvironment

Buat atau ubah Blazor markup antarmuka pengguna kesalahan:

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Di aplikasi Blazor Server , sesuaikan pengalaman dalam Pages/_Host.cshtml file. Contoh berikut menggunakan Pembantu Tag Lingkungan untuk mengonfigurasi pesan kesalahan untuk lingkungan yang berbeda.

Di aplikasi Blazor Server , sesuaikan pengalaman dalam Pages/_Layout.cshtml file. Contoh berikut menggunakan Pembantu Tag Lingkungan untuk mengonfigurasi pesan kesalahan untuk lingkungan yang berbeda.

Di aplikasi Blazor Server , sesuaikan pengalaman dalam Pages/_Host.cshtml file. Contoh berikut menggunakan Pembantu Tag Lingkungan untuk mengonfigurasi pesan kesalahan untuk lingkungan yang berbeda.

Buat atau ubah Blazor markup antarmuka pengguna kesalahan:

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Di aplikasi Blazor WebAssembly , sesuaikan pengalaman dalam wwwroot/index.html file:

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Elemen blazor-error-ui ini biasanya disembunyikan karena adanya display: none gaya blazor-error-ui kelas CSS di stylesheet yang dihasilkan secara otomatis dari aplikasi. Ketika kesalahan terjadi, kerangka kerja berlaku display: block untuk elemen .

Elemen blazor-error-ui biasanya tersembunyi karena adanya display: none gaya blazor-error-ui kelas CSS di lembar gaya situs di wwwroot/css folder. Ketika kesalahan terjadi, kerangka kerja berlaku display: block untuk elemen .

Kesalahan sirkuit terperinci

Bagian ini berlaku untuk Blazor Web Apps yang beroperasi melalui sirkuit.

Bagian ini berlaku untuk Blazor Server aplikasi.

Kesalahan sisi klien tidak menyertakan tumpukan panggilan dan tidak memberikan detail tentang penyebab kesalahan, tetapi log server berisi informasi tersebut. Untuk tujuan pengembangan, informasi kesalahan sirkuit sensitif dapat disediakan untuk klien dengan mengaktifkan kesalahan terperinci.

Atur CircuitOptions.DetailedErrors ke true. Untuk informasi selengkapnya dan contohnya, lihat panduan ASP.NET CoreBlazorSignalR.

Alternatif untuk pengaturan CircuitOptions.DetailedErrors adalah mengatur DetailedErrors kunci konfigurasi ke true dalam file pengaturan lingkungan aplikasi Development (appsettings.Development.json). Selain itu, atur SignalR pengelogan sisi server (Microsoft.AspNetCore.SignalR) ke Debug atau Lacak untuk pengelogan terperinci SignalR .

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Kunci DetailedErrors konfigurasi juga dapat diatur ke true menggunakan ASPNETCORE_DETAILEDERRORS variabel lingkungan dengan nilai true pada Development/Staging server lingkungan atau pada sistem lokal Anda.

Peringatan

Selalu hindari mengekspos informasi kesalahan kepada klien di Internet, yang merupakan risiko keamanan.

Kesalahan terperinci untuk Razor penyajian sisi server komponen

Bagian ini berlaku untuk Blazor Web Apps.

RazorComponentsServiceOptions.DetailedErrors Gunakan opsi untuk mengontrol menghasilkan informasi terperinci tentang kesalahan untuk Razor penyajian sisi server komponen. Nilai defaultnya adalah false.

Contoh berikut memungkinkan kesalahan terperinci:

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

Peringatan

Hanya aktifkan kesalahan terperinci di Development lingkungan. Kesalahan terperinci mungkin berisi informasi sensitif tentang aplikasi yang dapat digunakan pengguna berbahaya dalam serangan.

Contoh sebelumnya memberikan tingkat keamanan dengan menetapkan nilai DetailedErrors berdasarkan nilai yang dikembalikan oleh IsDevelopment. Saat aplikasi berada di Development lingkungan, DetailedErrors diatur ke true. Pendekatan ini tidak mudah karena dimungkinkan untuk menghosting aplikasi produksi di server publik di Development lingkungan.

Mengelola pengecualian yang tidak tertangani dalam kode pengembang

Agar aplikasi berlanjut setelah kesalahan, aplikasi harus memiliki logika penanganan kesalahan. Bagian selanjutnya dari artikel ini menjelaskan sumber potensial pengecualian yang tidak tertangani.

Dalam produksi, jangan merender pesan pengecualian kerangka kerja atau jejak tumpukan di UI. Merender pesan pengecualian atau jejak tumpukan dapat:

  • Mengungkapkan informasi sensitif kepada pengguna akhir.
  • Bantu pengguna berbahaya menemukan kelemahan di aplikasi yang dapat membahayakan keamanan aplikasi, server, atau jaringan.

Pengecualian yang tidak tertangani untuk sirkuit

Bagian ini berlaku untuk aplikasi sisi server yang beroperasi melalui sirkuit.

Razor komponen dengan interaktivitas server diaktifkan bersifat stateful di server. Saat pengguna berinteraksi dengan komponen di server, mereka mempertahankan koneksi ke server yang dikenal sebagai sirkuit. Sirkuit ini menyimpan instans komponen aktif, ditambah banyak aspek status lainnya, seperti:

  • Output komponen yang dirender terbaru.
  • Kumpulan delegasi penanganan peristiwa saat ini yang dapat dipicu oleh peristiwa sisi klien.

Jika pengguna membuka aplikasi di beberapa tab browser, pengguna membuat beberapa sirkuit independen.

Blazor memperlakukan pengecualian yang paling tidak tertangani sebagai fatal bagi sirkuit tempat mereka terjadi. Jika sirkuit dihentikan karena pengecualian yang tidak tertangani, pengguna hanya dapat terus berinteraksi dengan aplikasi dengan memuat ulang halaman untuk membuat sirkuit baru. Sirkuit di luar sirkuit yang dihentikan, yang merupakan sirkuit untuk pengguna lain atau tab browser lainnya, tidak terpengaruh. Skenario ini mirip dengan aplikasi desktop yang mengalami crash. Aplikasi yang mengalami crash harus dimulai ulang, tetapi aplikasi lain tidak terpengaruh.

Kerangka kerja mengakhiri sirkuit ketika pengecualian yang tidak tertangani terjadi karena alasan berikut:

  • Pengecualian yang tidak tertangani sering meninggalkan sirkuit dalam keadaan tidak terdefinisi.
  • Operasi normal aplikasi tidak dapat dijamin setelah pengecualian yang tidak tertangani.
  • Kerentanan keamanan dapat muncul di aplikasi jika sirkuit berlanjut dalam status tidak terdefinisi.

Penanganan pengecualian global

Untuk pendekatan menangani pengecualian secara global, lihat bagian berikut:

  • Batas kesalahan: Berlaku untuk semua Blazor aplikasi.
  • Penanganan pengecualian global alternatif: Berlaku untuk Blazor Server, , Blazor WebAssemblydan Blazor Web Apps (8.0 atau yang lebih baru) yang mengadopsi mode render interaktif global.

Batas kesalahan

Batas kesalahan memberikan pendekatan yang nyaman untuk menangani pengecualian. Komponen ErrorBoundary :

  • Merender konten turunannya ketika kesalahan belum terjadi.
  • Merender UI kesalahan ketika pengecualian yang tidak tertangani dilemparkan oleh komponen apa pun dalam batas kesalahan.

Untuk menentukan batas kesalahan, gunakan ErrorBoundary komponen untuk membungkus satu atau beberapa komponen lainnya. Batas kesalahan mengelola pengecualian yang tidak tertangani yang dilemparkan oleh komponen yang dibungkusnya.

<ErrorBoundary>
    ...
</ErrorBoundary>

Untuk menerapkan batas kesalahan dengan cara global, tambahkan batas di sekitar konten isi tata letak utama aplikasi.

Di MainLayout.razor:

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

Di Blazor Web Apps dengan batas kesalahan hanya diterapkan ke komponen statis MainLayout , batas hanya aktif selama penyajian sisi server statis (SSR statis). Batas tidak diaktifkan hanya karena komponen lebih jauh ke hierarki komponen bersifat interaktif.

Mode render interaktif tidak dapat diterapkan ke MainLayout komponen karena parameter komponen Body adalah RenderFragment delegasi, yang merupakan kode sewenang-wenang dan tidak dapat diserialisasikan. Untuk mengaktifkan interaktivitas secara luas untuk MainLayout komponen dan komponen lainnya lebih jauh ke hierarki komponen, aplikasi harus mengadopsi mode render interaktif global dengan menerapkan mode render interaktif ke HeadOutlet instans komponen dan Routes di komponen root aplikasi, yang biasanya App merupakan komponen. Contoh berikut mengadopsi mode render Server Interaktif (InteractiveServer) secara global.

Di Components/App.razor:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Jika Anda lebih suka tidak mengaktifkan interaktivitas global, letakkan batas kesalahan lebih jauh ke hierarki komponen. Konsep penting yang perlu diingat adalah bahwa di mana pun batas kesalahan ditempatkan:

  • Jika komponen tempat batas kesalahan ditempatkan tidak interaktif, batas kesalahan hanya mampu mengaktifkan di server selama SSR statis. Misalnya, batas dapat diaktifkan ketika kesalahan dilemparkan dalam metode siklus hidup komponen tetapi tidak untuk peristiwa yang dipicu oleh interaktivitas pengguna dalam komponen, seperti kesalahan yang dilemparkan oleh handler klik tombol.
  • Jika komponen tempat batas kesalahan ditempatkan interaktif, batas kesalahan mampu mengaktifkan komponen interaktif yang dibungkusnya.

Catatan

Pertimbangan sebelumnya tidak relevan untuk aplikasi mandiri Blazor WebAssembly karena penyajian Blazor WebAssembly sisi klien (CSR) aplikasi sepenuhnya interaktif.

Pertimbangkan contoh berikut, di mana pengecualian yang dilemparkan oleh komponen penghitung yang disematkan ditangkap oleh batas kesalahan dalam Home komponen, yang mengadopsi mode render interaktif.

EmbeddedCounter.razor:

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}

Home.razor:

@page "/"
@rendermode InteractiveServer

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

Pertimbangkan contoh berikut, di mana pengecualian yang dilemparkan oleh komponen penghitung yang disematkan ditangkap oleh batas kesalahan dalam Home komponen.

EmbeddedCounter.razor:

<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}

Home.razor:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>

Jika pengecualian yang tidak tertangani dilemparkan selama currentCount lebih dari lima:

  • Kesalahan dicatat secara normal (System.InvalidOperationException: Current count is too big!).
  • Pengecualian ditangani oleh batas kesalahan.
  • UI kesalahan default dirender oleh batas kesalahan.

Secara default, ErrorBoundary komponen merender elemen kosong <div> menggunakan blazor-error-boundary kelas CSS untuk konten kesalahannya. Warna, teks, dan ikon untuk antarmuka pengguna default didefinisikan dalam lembar gaya aplikasi di wwwroot folder, sehingga Anda bebas untuk menyesuaikan antarmuka pengguna kesalahan.

UI kesalahan default yang dirender oleh batas kesalahan, yang memiliki latar belakang merah, teks 'Kesalahan telah terjadi,' dan ikon perhatian kuning dengan tanda seru di dalamnya.

Untuk mengubah konten kesalahan default:

Contoh berikut membungkus EmbeddedCounter komponen dan menyediakan konten kesalahan kustom:

<ErrorBoundary>
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

Untuk contoh sebelumnya, lembar gaya aplikasi mungkin menyertakan errorUI kelas CSS untuk menata konten. Konten kesalahan dirender dari ErrorContent properti tanpa elemen tingkat blok. Elemen tingkat blok, seperti elemen pembagian (<div>) atau paragraf (<p>), dapat membungkus markup konten kesalahan, tetapi tidak diperlukan.

Jika batas kesalahan ditentukan dalam tata letak aplikasi, antarmuka pengguna kesalahan terlihat terlepas dari halaman mana pengguna menavigasi setelah kesalahan terjadi. Sebaiknya cakupan batas kesalahan secara sempit dalam sebagian besar skenario. Jika Anda secara luas mencakup batas kesalahan, Anda dapat mengatur ulang ke status non-kesalahan pada peristiwa navigasi halaman berikutnya dengan memanggil metode batas Recover kesalahan.

Di MainLayout.razor:

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

Untuk menghindari perulangan tak terbatas di mana pemulihan hanya merender komponen yang melemparkan kesalahan lagi, jangan panggil Recover dari logika penyajian. Hanya panggil Recover saat:

  • Pengguna melakukan gerakan UI, seperti memilih tombol untuk menunjukkan bahwa mereka ingin mencoba kembali prosedur atau ketika pengguna menavigasi ke komponen baru.
  • Logika tambahan yang dijalankan juga menghapus pengecualian. Ketika komponen dirender ulang, kesalahan tidak terulang kembali.

Penanganan pengecualian global alternatif

Pendekatan yang dijelaskan di bagian ini berlaku untuk Blazor Server, , Blazor WebAssemblydan Blazor Web Apps yang mengadopsi mode render interaktif global (InteractiveServer, InteractiveWebAssembly, atau InteractiveAuto). Pendekatan ini tidak berfungsi dengan Blazor Web Apps yang mengadopsi mode render per halaman/komponen atau penyajian sisi server statis (SSR statis) karena pendekatan bergantung pada CascadingValue/CascadingParameter, yang tidak berfungsi di seluruh batas mode render atau dengan komponen yang mengadopsi SSR statis.

Alternatif untuk menggunakan Batas kesalahan (ErrorBoundary) adalah meneruskan komponen kesalahan kustom sebagai CascadingValue komponen ke anak. Keuntungan menggunakan komponen daripada menggunakan layanan yang disuntikkan atau implementasi pencatat kustom adalah bahwa komponen bertingkat dapat merender konten dan menerapkan gaya CSS saat kesalahan terjadi.

Contoh komponen berikut ProcessError hanya mencatat kesalahan, tetapi metode komponen dapat memproses kesalahan dengan cara apa pun yang diperlukan oleh aplikasi, termasuk melalui penggunaan beberapa metode pemrosesan kesalahan.

ProcessError.razor:

@inject ILogger<ProcessError> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if LogError directly participates in 
        // rendering. If LogError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

Catatan

Untuk informasi selengkapnya tentang RenderFragment, lihat komponen ASP.NET CoreRazor.

Saat menggunakan pendekatan ini di Blazor Aplikasi Web, buka Routes komponen dan bungkus Router komponen (<Router>...</Router>) dengan ProcessError komponen . Hal ini memungkinkan ProcessError komponen untuk melakukan cascade ke komponen aplikasi mana pun di mana ProcessError komponen diterima sebagai CascadingParameter.

Di Routes.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Saat menggunakan pendekatan ini dalam aplikasi Blazor Server atau Blazor WebAssembly , buka App komponen , bungkus Router komponen (<Router>...</Router>) dengan ProcessError komponen . Hal ini memungkinkan ProcessError komponen untuk melakukan cascade ke komponen aplikasi mana pun di mana ProcessError komponen diterima sebagai CascadingParameter.

Di App.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Untuk memproses kesalahan dalam komponen:

  • Menunjuk ProcessError komponen sebagai CascadingParameter di @code blok. Dalam contoh Counter komponen dalam aplikasi berdasarkan Blazor templat proyek, tambahkan properti berikut ProcessError :

    [CascadingParameter]
    public ProcessError? ProcessError { get; set; }
    
  • Panggil metode pemrosesan kesalahan di blok mana pun catch dengan jenis pengecualian yang sesuai. Komponen contoh ProcessError hanya menawarkan satu LogError metode, tetapi komponen pemrosesan kesalahan dapat menyediakan sejumlah metode pemrosesan kesalahan untuk mengatasi persyaratan pemrosesan kesalahan alternatif di seluruh aplikasi. Contoh blok komponen @code berikut Counter mencakup ProcessError parameter bertingkat dan menjebak pengecualian untuk pengelogan ketika jumlahnya lebih besar dari lima:

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public ProcessError? ProcessError { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                ProcessError?.LogError(ex);
            }
        }
    }
    

Kesalahan yang dicatat:

fail: {COMPONENT NAMESPACE}.ProcessError[0]
ProcessError.LogError: System.InvalidOperationException Message: Current count is over five!

Jika metode secara LogError langsung berpartisipasi dalam penyajian, seperti memperlihatkan bilah pesan kesalahan kustom atau mengubah gaya CSS elemen yang dirender, panggil StateHasChanged di akhir LogError metode untuk merender UI.

Karena pendekatan di bagian ini menangani kesalahan dengan try-catch pernyataan, koneksi aplikasi SignalR antara klien dan server tidak rusak ketika kesalahan terjadi dan sirkuit tetap hidup. Pengecualian lain yang tidak tertangani tetap fatal bagi sirkuit. Untuk informasi selengkapnya, lihat bagian tentang bagaimana sirkuit bereaksi terhadap pengecualian yang tidak tertangani.

Aplikasi dapat menggunakan komponen pemrosesan kesalahan sebagai nilai berkala untuk memproses kesalahan dengan cara terpusat.

Komponen berikut ProcessError meneruskan dirinya sebagai CascadingValue komponen ke anak. Contoh berikut hanya mencatat kesalahan, tetapi metode komponen dapat memproses kesalahan dengan cara apa pun yang diperlukan oleh aplikasi, termasuk melalui penggunaan beberapa metode pemrosesan kesalahan. Keuntungan menggunakan komponen daripada menggunakan layanan yang disuntikkan atau implementasi pencatat kustom adalah bahwa komponen bertingkat dapat merender konten dan menerapkan gaya CSS saat kesalahan terjadi.

ProcessError.razor:

@using Microsoft.Extensions.Logging
@inject ILogger<ProcessError> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

Catatan

Untuk informasi selengkapnya tentang RenderFragment, lihat komponen ASP.NET CoreRazor.

App Dalam komponen, bungkus Router komponen dengan ProcessError komponen . Hal ini memungkinkan ProcessError komponen untuk melakukan cascade ke komponen aplikasi mana pun di mana ProcessError komponen diterima sebagai CascadingParameter.

App.razor:

<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>

Untuk memproses kesalahan dalam komponen:

  • Tetapkan ProcessError komponen sebagai CascadingParameter dalam @code blok:

    [CascadingParameter]
    public ProcessError ProcessError { get; set; }
    
  • Panggil metode pemrosesan kesalahan di blok mana pun catch dengan jenis pengecualian yang sesuai. Komponen contoh ProcessError hanya menawarkan satu LogError metode, tetapi komponen pemrosesan kesalahan dapat menyediakan sejumlah metode pemrosesan kesalahan untuk mengatasi persyaratan pemrosesan kesalahan alternatif di seluruh aplikasi.

    try
    {
        ...
    }
    catch (Exception ex)
    {
        ProcessError.LogError(ex);
    }
    

Menggunakan komponen dan LogError metode contoh ProcessError sebelumnya, konsol alat pengembang browser menunjukkan kesalahan yang terperangkap dan dicatat:

fail: {COMPONENT NAMESPACE}.Shared.ProcessError[0]
ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.

Jika metode secara LogError langsung berpartisipasi dalam penyajian, seperti memperlihatkan bilah pesan kesalahan kustom atau mengubah gaya CSS elemen yang dirender, panggil StateHasChanged di akhir LogError metode untuk merender UI.

Karena pendekatan di bagian ini menangani kesalahan dengan try-catch pernyataan, Blazor koneksi aplikasi SignalR antara klien dan server tidak rusak ketika kesalahan terjadi dan sirkuit tetap hidup. Pengecualian yang tidak tertangani berakibat fatal bagi sirkuit. Untuk informasi selengkapnya, lihat bagian tentang bagaimana sirkuit bereaksi terhadap pengecualian yang tidak tertangani.

Kesalahan log dengan penyedia persisten

Jika terjadi pengecualian yang tidak tertangani, pengecualian dicatat ke ILogger instans yang dikonfigurasi dalam kontainer layanan. Secara default, Blazor aplikasi mencatat ke output konsol dengan Penyedia Pengelogan Konsol. Pertimbangkan untuk masuk ke lokasi di server (atau API web backend untuk aplikasi sisi klien) dengan penyedia yang mengelola ukuran log dan rotasi log. Atau, aplikasi dapat menggunakan layanan Manajemen Performa Aplikasi (APM), seperti Azure Application Insights (Azure Monitor).

Catatan

Fitur Application Insights asli untuk mendukung aplikasi sisi klien dan dukungan kerangka kerja asli Blazor untuk Google Analytics mungkin tersedia dalam rilis teknologi ini di masa mendatang. Untuk informasi selengkapnya, lihat Mendukung App Insights di Blazor Sisi Klien WASM (microsoft/ApplicationInsights-dotnet #2143) dan Analitik dan diagnostik Web (termasuk tautan ke implementasi komunitas) (dotnet/aspnetcore #5461). Sementara itu, aplikasi sisi klien dapat menggunakan Application Insights JavaScript SDK dengan JS interop untuk mencatat kesalahan langsung ke Application Insights dari aplikasi sisi klien.

Selama pengembangan dalam aplikasi yang Blazor beroperasi melalui sirkuit, aplikasi biasanya mengirim detail lengkap pengecualian ke konsol browser untuk membantu penelusuran kesalahan. Dalam produksi, kesalahan terperinci tidak dikirim ke klien, tetapi detail lengkap pengecualian dicatat di server.

Anda harus memutuskan insiden mana yang akan dicatat dan tingkat keparahan insiden yang dicatat. Pengguna yang bermusuhan mungkin dapat memicu kesalahan dengan sengaja. Misalnya, jangan mencatat insiden dari kesalahan di mana yang tidak diketahui ProductId disediakan dalam URL komponen yang menampilkan detail produk. Tidak semua kesalahan harus diperlakukan sebagai insiden untuk pengelogan.

Untuk informasi lebih lanjut, baca artikel berikut:

‡Berlaku untuk aplikasi sisi Blazor server dan aplikasi ASP.NET Core sisi server lainnya yang merupakan aplikasi backend API web untuk Blazor. Aplikasi sisi klien dapat menjebak dan mengirim informasi kesalahan pada klien ke API web, yang mencatat informasi kesalahan ke penyedia pengelogan persisten.

Jika terjadi pengecualian yang tidak tertangani, pengecualian dicatat ke ILogger instans yang dikonfigurasi dalam kontainer layanan. Secara default, Blazor aplikasi mencatat ke output konsol dengan Penyedia Pengelogan Konsol. Pertimbangkan untuk masuk ke lokasi yang lebih permanen di server dengan mengirim informasi kesalahan ke API web backend yang menggunakan penyedia pengelogan dengan manajemen ukuran log dan rotasi log. Atau, aplikasi API web backend dapat menggunakan layanan Manajemen Performa Aplikasi (APM), seperti Azure Application Insights (Azure Monitor)†, untuk merekam informasi kesalahan yang diterimanya dari klien.

Anda harus memutuskan insiden mana yang akan dicatat dan tingkat keparahan insiden yang dicatat. Pengguna yang bermusuhan mungkin dapat memicu kesalahan dengan sengaja. Misalnya, jangan mencatat insiden dari kesalahan di mana yang tidak diketahui ProductId disediakan dalam URL komponen yang menampilkan detail produk. Tidak semua kesalahan harus diperlakukan sebagai insiden untuk pengelogan.

Untuk informasi lebih lanjut, baca artikel berikut:

†Native Application Insights untuk mendukung aplikasi sisi klien dan dukungan kerangka kerja asli Blazor untuk Google Analytics mungkin tersedia dalam rilis teknologi ini di masa mendatang. Untuk informasi selengkapnya, lihat Mendukung App Insights di Blazor Sisi Klien WASM (microsoft/ApplicationInsights-dotnet #2143) dan Analitik dan diagnostik Web (termasuk tautan ke implementasi komunitas) (dotnet/aspnetcore #5461). Sementara itu, aplikasi sisi klien dapat menggunakan Application Insights JavaScript SDK dengan JS interop untuk mencatat kesalahan langsung ke Application Insights dari aplikasi sisi klien.

‡Berlaku untuk aplikasi ASP.NET Core sisi server yang merupakan aplikasi backend API web untuk Blazor aplikasi. Aplikasi sisi klien menjebak dan mengirim informasi kesalahan ke API web, yang mencatat informasi kesalahan ke penyedia pengelogan persisten.

Tempat di mana kesalahan dapat terjadi

Kerangka kerja dan kode aplikasi dapat memicu pengecualian yang tidak tertangani di salah satu lokasi berikut, yang dijelaskan lebih lanjut di bagian berikut dari artikel ini:

Instansiasi komponen

Saat Blazor membuat instans komponen:

  • Konstruktor komponen dipanggil.
  • Konstruktor layanan DI yang disediakan ke konstruktor komponen melalui @inject direktif atau [Inject] atribut dipanggil.

Kesalahan dalam konstruktor yang dijalankan atau setter untuk properti apa pun [Inject] menghasilkan pengecualian yang tidak tertangani dan menghentikan kerangka kerja membuat instans komponen. Jika aplikasi beroperasi melalui sirkuit, sirkuit gagal. Jika logika konstruktor dapat melemparkan pengecualian, aplikasi harus menjebak pengecualian menggunakan try-catch pernyataan dengan penanganan kesalahan dan pengelogan.

Metode siklus hidup

Selama masa pakai komponen, Blazor memanggil metode siklus hidup. Jika ada metode siklus hidup yang melemparkan pengecualian, secara sinkron atau asinkron, pengecualiannya fatal bagi sirkuit. Agar komponen menangani kesalahan dalam metode siklus hidup, tambahkan logika penanganan kesalahan.

Dalam contoh berikut di mana OnParametersSetAsync memanggil metode untuk mendapatkan produk:

  • Pengecualian yang dilemparkan dalam metode ditangani ProductRepository.GetProductByIdAsync oleh try-catch pernyataan.
  • catch Ketika blok dijalankan:
    • loadFailed diatur ke true, yang digunakan untuk menampilkan pesan kesalahan kepada pengguna.
    • Kesalahan dicatat.
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

Logika penyajian

Markup deklaratif dalam file komponen () dikompilasi Razor ke dalam metode C# yang disebut BuildRenderTree..razor Saat komponen merender, BuildRenderTree menjalankan dan membangun struktur data yang menjelaskan elemen, teks, dan komponen turunan dari komponen yang dirender.

Logika penyajian dapat melemparkan pengecualian. Contoh skenario ini terjadi ketika @someObject.PropertyName dievaluasi tetapi @someObject adalah null. Untuk Blazor aplikasi yang beroperasi melalui sirkuit, pengecualian yang tidak tertangani yang dilemparkan oleh logika penyajian berakibat fatal bagi sirkuit aplikasi.

Untuk mencegah NullReferenceException dalam logika penyajian, periksa null objek sebelum mengakses anggotanya. Dalam contoh berikut, person.Address properti tidak diakses jika person.Address adalah null:

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

Kode sebelumnya mengasumsikan bahwa person bukan null. Seringkali, struktur kode menjamin bahwa objek ada pada saat komponen dirender. Dalam kasus tersebut, tidak perlu memeriksa null logika penyajian. Dalam contoh sebelumnya, person mungkin dijamin ada karena person dibuat saat komponen dibuat, seperti yang ditunjukkan contoh berikut:

@code {
    private Person person = new();

    ...
}

Penangan kejadian

Kode sisi klien memicu pemanggilan kode C# saat penanganan aktivitas dibuat menggunakan:

  • @onclick
  • @onchange
  • Atribut lain @on...
  • @bind

Kode penanganan aktivitas mungkin melemparkan pengecualian yang tidak tertangani dalam skenario ini.

Jika aplikasi memanggil kode yang dapat gagal karena alasan eksternal, pengecualian perangkap menggunakan try-catch pernyataan dengan penanganan kesalahan dan pengelogan.

Jika penanganan aktivitas melemparkan pengecualian yang tidak tertangani (misalnya, kueri database gagal) yang tidak terperangkap dan ditangani oleh kode pengembang:

  • Kerangka kerja mencatat pengecualian.
  • Dalam aplikasi yang Blazor beroperasi melalui sirkuit, pengecualiannya berakibat fatal bagi sirkuit aplikasi.

Pembuangan komponen

Komponen dapat dihapus dari UI, misalnya, karena pengguna telah menavigasi ke halaman lain. Ketika komponen yang menerapkan System.IDisposable dihapus dari UI, kerangka kerja memanggil metode komponen Dispose .

Jika metode komponen Dispose melemparkan pengecualian yang tidak tertangani dalam aplikasi yang Blazor beroperasi melalui sirkuit, pengecualiannya berakibat fatal bagi sirkuit aplikasi.

Jika logika pembuangan dapat melemparkan pengecualian, aplikasi harus menjebak pengecualian menggunakan try-catch pernyataan dengan penanganan kesalahan dan pengelogan.

Untuk informasi selengkapnya tentang pembuangan komponen, lihat siklus hidup komponen ASP.NET CoreRazor.

Interop JavaScript

IJSRuntime terdaftar oleh Blazor kerangka kerja. IJSRuntime.InvokeAsync memungkinkan kode .NET untuk melakukan panggilan asinkron ke runtime JavaScript (JS) di browser pengguna.

Kondisi berikut berlaku untuk penanganan kesalahan dengan InvokeAsync:

  • Jika panggilan gagal InvokeAsync secara sinkron, pengecualian .NET terjadi. Panggilan ke InvokeAsync mungkin gagal, misalnya, karena argumen yang disediakan tidak dapat diserialisasikan. Kode pengembang harus menangkap pengecualian. Jika kode aplikasi dalam metode penanganan aktivitas atau siklus hidup komponen tidak menangani pengecualian dalam aplikasi yang Blazor beroperasi melalui sirkuit, pengecualian yang dihasilkan berakibat fatal bagi sirkuit aplikasi.
  • Jika panggilan gagal InvokeAsync secara asinkron, .NET Task gagal. Panggilan ke InvokeAsync mungkin gagal, misalnya, karena JSkode -side melempar pengecualian atau mengembalikan Promise yang diselesaikan sebagai rejected. Kode pengembang harus menangkap pengecualian. Jika menggunakan await operator, pertimbangkan untuk membungkus panggilan metode dalam try-catch pernyataan dengan penanganan kesalahan dan pengelogan. Jika tidak, dalam aplikasi yang Blazor beroperasi melalui sirkuit, kode yang gagal menghasilkan pengecualian yang tidak tertangani yang fatal bagi sirkuit aplikasi.
  • Secara default, panggilan ke InvokeAsync harus diselesaikan dalam periode tertentu atau waktu panggilan habis. Periode batas waktu default adalah satu menit. Batas waktu melindungi kode dari kehilangan konektivitas jaringan atau JS kode yang tidak pernah mengirim kembali pesan penyelesaian. Jika waktu panggilan habis, hasilnya System.Threading.Tasks gagal dengan OperationCanceledException. Perangkap dan proses pengecualian dengan pengelogan.

Demikian pula, JS kode dapat memulai panggilan ke metode .NET yang ditunjukkan oleh [JSInvokable] atribut . Jika metode .NET ini melemparkan pengecualian yang tidak tertangani:

  • Dalam aplikasi yang Blazor beroperasi melalui sirkuit, pengecualian tidak diperlakukan sebagai fatal bagi sirkuit aplikasi.
  • - JSsisi Promise ditolak.

Anda memiliki opsi untuk menggunakan kode penanganan kesalahan di sisi .NET atau sisi JS panggilan metode.

Untuk informasi lebih lanjut, baca artikel berikut:

Pra-penyajian

Razor komponen telah dirender secara default sehingga markup HTML yang dirender dikembalikan sebagai bagian dari permintaan HTTP awal pengguna.

Dalam aplikasi yang Blazor beroperasi melalui sirkuit, pra-penyajian berfungsi dengan:

  • Membuat sirkuit baru untuk semua komponen yang telah dirender sebelumnya yang merupakan bagian dari halaman yang sama.
  • Membuat HTML awal.
  • Memperlakukan sirkuit sampai disconnected browser pengguna membuat SignalR koneksi kembali ke server yang sama. Ketika koneksi dibuat, interaktivitas pada sirkuit dilanjutkan dan markup HTML komponen diperbarui.

Untuk komponen sisi klien yang telah dirender sebelumnya, prarender bekerja dengan:

  • Menghasilkan HTML awal di server untuk semua komponen yang telah dirender sebelumnya yang merupakan bagian dari halaman yang sama.
  • Membuat komponen interaktif pada klien setelah browser memuat kode yang dikompilasi aplikasi dan runtime .NET (jika belum dimuat) di latar belakang.

Jika komponen melemparkan pengecualian yang tidak tertangani selama pra-penyajian, misalnya, selama metode siklus hidup atau dalam logika penyajian:

  • Dalam aplikasi yang Blazor beroperasi melalui sirkuit, pengecualiannya berakibat fatal bagi sirkuit. Untuk komponen sisi klien yang telah dirender sebelumnya, pengecualian mencegah penyajian komponen.
  • Pengecualian dilemparkan ke tumpukan panggilan dari ComponentTagHelper.

Dalam keadaan normal saat pra-penyajian gagal, terus membangun dan merender komponen tidak masuk akal karena komponen yang berfungsi tidak dapat dirender.

Untuk mentolerir kesalahan yang mungkin terjadi selama pra-penyajian, logika penanganan kesalahan harus ditempatkan di dalam komponen yang dapat melemparkan pengecualian. Gunakan try-catch pernyataan dengan penanganan kesalahan dan pengelogan. Alih-alih membungkus ComponentTagHelper dalam try-catch pernyataan, tempatkan logika penanganan kesalahan dalam komponen yang dirender oleh ComponentTagHelper.

Skenario tingkat lanjut

Penyajian rekursif

Komponen dapat disarangkan secara rekursif. Ini berguna untuk mewakili struktur data rekursif. Misalnya, TreeNode komponen dapat merender lebih TreeNode banyak komponen untuk setiap anak simpul.

Saat merender secara rekursif, hindari pola pengkodian yang mengakibatkan rekursi tak terbatas:

  • Jangan merender struktur data secara rekursif yang berisi siklus. Misalnya, jangan merender simpul pohon yang anak-anaknya menyertakan dirinya sendiri.
  • Jangan membuat rantai tata letak yang berisi siklus. Misalnya, jangan membuat tata letak yang tata letaknya sendiri.
  • Jangan izinkan pengguna akhir melanggar invarian rekursi (aturan) melalui entri data berbahaya atau panggilan interop JavaScript.

Perulangan tak terbatas selama penyajian:

  • Menyebabkan proses penyajian berlanjut selamanya.
  • Setara dengan membuat perulangan yang tidak ditentukan.

Dalam skenario ini, Blazor gagal dan biasanya mencoba untuk:

  • Konsumsi waktu CPU sebanyak yang diizinkan oleh sistem operasi, tanpa batas waktu.
  • Mengonsumsi memori dalam jumlah tak terbatas. Mengonsumsi memori tak terbatas setara dengan skenario di mana perulangan yang tidak ditentukan menambahkan entri ke koleksi pada setiap perulangan.

Untuk menghindari pola rekursi tak terbatas, pastikan bahwa kode rendering rekursif berisi kondisi penghentian yang sesuai.

Logika pohon render kustom

Sebagian besar Razor komponen diimplementasikan sebagai Razor file komponen (.razor) dan dikompilasi oleh kerangka kerja untuk menghasilkan logika yang beroperasi pada RenderTreeBuilder untuk merender output mereka. Namun, pengembang dapat menerapkan logika secara RenderTreeBuilder manual menggunakan kode C# prosedural. Untuk informasi selengkapnya, lihat skenario lanjutan ASP.NET Core Blazor (konstruksi pohon render).

Peringatan

Penggunaan logika pembuat pohon render manual dianggap sebagai skenario tingkat lanjut dan tidak aman, tidak disarankan untuk pengembangan komponen umum.

Jika RenderTreeBuilder kode ditulis, pengembang harus menjamin kebenaran kode. Misalnya, pengembang harus memastikan bahwa:

  • Panggilan ke OpenElement dan CloseElement diseimbangkan dengan benar.
  • Atribut hanya ditambahkan di tempat yang benar.

Logika pembangun pohon render manual yang salah dapat menyebabkan perilaku yang tidak terdefinisi secara arbitrer, termasuk crash, aplikasi atau server macet, dan kerentanan keamanan.

Pertimbangkan logika pembuat pohon render manual pada tingkat kompleksitas yang sama dan dengan tingkat bahaya yang sama seperti menulis kode perakitan atau instruksi Microsoft Intermediate Language (MSIL) secara manual.

Sumber Daya Tambahan:

†Aplikasi ke backend ASP.NET aplikasi API web Core yang digunakan aplikasi sisi Blazor klien untuk pengelogan.