ASP.NET Nilai dan parameter berskala 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 mengalirkan data dari komponen leluhur Razor ke komponen keturunan.
Nilai dan parameter berskala menyediakan cara mudah untuk mengalirkan data ke hierarki komponen dari komponen leluhur ke sejumlah komponen keturunan. Tidak seperti parameter Komponen, nilai dan parameter berskala tidak memerlukan penetapan atribut untuk setiap komponen turunan tempat data digunakan. Nilai dan parameter berskala juga memungkinkan komponen untuk berkoordinasi satu sama lain di seluruh hierarki komponen.
Catatan
Contoh kode dalam artikel ini mengadopsi jenis referensi nullable (NRTs) dan .NET compiler null-state static analysis, yang didukung di ASP.NET Core di .NET 6 atau yang lebih baru. Saat menargetkan ASP.NET Core 5.0 atau yang lebih lama, hapus penunjukan jenis null (?
) dari CascadingType?
jenis , , @ActiveTab?
, ITab?
RenderFragment?
, TabSet?
, dan string?
dalam contoh artikel.
Nilai berskala tingkat akar
Nilai kaskading tingkat akar dapat didaftarkan untuk seluruh hierarki komponen. Nilai dan langganan berskala bernama untuk pemberitahuan pembaruan didukung.
Kelas berikut digunakan dalam contoh bagian ini.
Dalek.cs
:
// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0
namespace BlazorSample;
public class Dalek
{
public int Units { get; set; }
}
// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0
namespace BlazorSample;
public class Dalek
{
public int Units { get; set; }
}
Pendaftaran berikut dibuat dalam file aplikasi Program
dengan AddCascadingValue:
Dalek
dengan nilai properti untukUnits
didaftarkan sebagai nilai kaskading tetap.- Pendaftaran kedua
Dalek
dengan nilai properti yang berbeda untukUnits
diberi nama "AlphaGroup
".
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });
Komponen berikut Daleks
menampilkan nilai berskala.
Daleks.razor
:
@page "/daleks"
<PageTitle>Daleks</PageTitle>
<h1>Root-level Cascading Value Example</h1>
<ul>
<li>Dalek Units: @Dalek?.Units</li>
<li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>
<p>
Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>
@code {
[CascadingParameter]
public Dalek? Dalek { get; set; }
[CascadingParameter(Name = "AlphaGroup")]
public Dalek? AlphaGroupDalek { get; set; }
}
@page "/daleks"
<PageTitle>Daleks</PageTitle>
<h1>Root-level Cascading Value Example</h1>
<ul>
<li>Dalek Units: @Dalek?.Units</li>
<li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>
<p>
Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>
@code {
[CascadingParameter]
public Dalek? Dalek { get; set; }
[CascadingParameter(Name = "AlphaGroup")]
public Dalek? AlphaGroupDalek { get; set; }
}
Dalam contoh berikut, Dalek
terdaftar sebagai nilai berskala menggunakan CascadingValueSource<T>
, di mana <T>
adalah jenisnya. Bendera isFixed
menunjukkan apakah nilai diperbaiki. Jika false, semua penerima berlangganan pemberitahuan pembaruan, yang dikeluarkan dengan memanggil NotifyChangedAsync. Langganan membuat overhead dan mengurangi performa, jadi atur isFixed
ke true
jika nilai tidak berubah.
builder.Services.AddCascadingValue(sp =>
{
var dalek = new Dalek { Units = 789 };
var source = new CascadingValueSource<Dalek>(dalek, isFixed: false);
return source;
});
Peringatan
Mendaftarkan jenis komponen sebagai nilai kaskading tingkat akar tidak mendaftarkan layanan tambahan untuk jenis atau mengizinkan aktivasi layanan dalam komponen.
Perlakukan layanan yang diperlukan secara terpisah dari nilai kaskading, mendaftarkannya secara terpisah dari jenis berskala.
Hindari menggunakan AddCascadingValue untuk mendaftarkan jenis komponen sebagai nilai berskala. Sebagai gantinya <Router>...</Router>
, bungkus dalam Routes
komponen (Components/Routes.razor
) dengan komponen dan adopsi penyajian sisi server interaktif global (SSR interaktif). Misalnya, lihat bagian CascadingValue
komponen .
CascadingValue
komponen
Komponen leluhur menyediakan nilai berskala menggunakan Blazor komponen kerangka kerja CascadingValue
, yang membungkus subtree hierarki komponen dan memasok nilai tunggal ke semua komponen dalam subtreenya.
Contoh berikut menunjukkan alur informasi tema ke hierarki komponen untuk menyediakan kelas gaya CSS ke tombol dalam komponen turunan.
Kelas C# berikut ThemeInfo
menentukan informasi tema.
Catatan
Untuk contoh di bagian ini, namespace aplikasi adalah BlazorSample
. Saat bereksperimen dengan kode di aplikasi sampel Anda sendiri, ubah namespace aplikasi ke namespace aplikasi sampel Anda.
ThemeInfo.cs
:
namespace BlazorSample;
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
namespace BlazorSample;
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses
{
public class ThemeInfo
{
public string ButtonClass { get; set; }
}
}
namespace BlazorSample.UIThemeClasses
{
public class ThemeInfo
{
public string ButtonClass { get; set; }
}
}
Komponen tata letak berikut menentukan informasi tema (ThemeInfo
) sebagai nilai bertingkat untuk semua komponen yang membentuk isi Body tata letak properti. ButtonClass
diberi nilai btn-success
, yang merupakan gaya tombol Bootstrap. Setiap komponen turunan dalam hierarki komponen dapat menggunakan ButtonClass
properti melalui nilai berskala ThemeInfo
.
MainLayout.razor
:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<CascadingValue Value="theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<CascadingValue Value="theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<CascadingValue Value="@theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
</main>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<CascadingValue Value="@theme">
<div class="content px-4">
@Body
</div>
</CascadingValue>
</main>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<CascadingValue Value="@theme">
<div class="content px-4">
@Body
</div>
</CascadingValue>
</div>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<CascadingValue Value="theme">
<div class="content px-4">
@Body
</div>
</CascadingValue>
</div>
@code {
private ThemeInfo theme = new ThemeInfo { ButtonClass = "btn-success" };
}
Blazor Web Apps menyediakan pendekatan alternatif untuk nilai bertingkat yang berlaku lebih luas ke aplikasi daripada melengkungkannya melalui satu file tata letak:
Bungkus markup
Routes
komponen dalamCascadingValue
komponen untuk menentukan data sebagai nilai berskala untuk semua komponen aplikasi.Contoh data kaskade
ThemeInfo
berikut dariRoutes
komponen.Routes.razor
:<CascadingValue Value="theme"> <Router ...> ... </Router> </CascadingValue> @code { private ThemeInfo theme = new() { ButtonClass = "btn-success" }; }
Catatan
Membungkus
Routes
instans komponen dalamApp
komponen (Components/App.razor
) denganCascadingValue
komponen tidak didukung.Tentukan nilai berskala tingkat akar sebagai layanan dengan memanggil AddCascadingValue metode ekstensi pada penyusun kumpulan layanan.
Contoh data kaskade
ThemeInfo
berikut dariProgram
file.Program.cs
builder.Services.AddCascadingValue(sp => new ThemeInfo() { ButtonClass = "btn-primary" });
Untuk informasi selengkapnya, lihat bagian berikut dari artikel ini:
atribut [CascadingParameter]
Untuk menggunakan nilai kaskade, komponen descendent mendeklarasikan parameter kaskade menggunakan [CascadingParameter]
atribut . Nilai berskala terikat ke parameter berskala berdasarkan jenis. Kaskade beberapa nilai dengan jenis yang sama tercakup di bagian Beberapa nilai Berjendela nanti di artikel ini.
Komponen berikut mengikat ThemeInfo
nilai berskala ke parameter kaskading, secara opsional menggunakan nama yang sama dari ThemeInfo
. Parameter digunakan untuk mengatur kelas CSS untuk tombol Increment Counter (Themed)
.
ThemedCounter.razor
:
@page "/themed-counter"
<PageTitle>Themed Counter</PageTitle>
<h1>Themed Counter Example</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount() => currentCount++;
}
@page "/themed-counter"
<PageTitle>Themed Counter</PageTitle>
<h1>Themed Counter Example</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount() => currentCount++;
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
Mirip dengan parameter komponen reguler, komponen yang menerima parameter kaskading dirender saat nilai kaskading diubah. Misalnya, mengonfigurasi instans ThemedCounter
tema yang berbeda menyebabkan komponen dari bagian CascadingValue
komponen dirender ulang.
MainLayout.razor
:
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<CascadingValue Value="theme">
<article class="content px-4">
@Body
</article>
</CascadingValue>
<button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
private void ChangeToDarkTheme()
{
theme = new() { ButtonClass = "btn-secondary" };
}
}
CascadingValue<TValue>.IsFixed dapat digunakan untuk menunjukkan bahwa parameter berskala tidak berubah setelah inisialisasi.
Nilai/parameter berskala dan batas mode render
Parameter berskala tidak meneruskan data di seluruh batas mode render:
Sesi interaktif berjalan dalam konteks yang berbeda dari halaman yang menggunakan penyajian sisi server statis (SSR statis). Tidak ada persyaratan bahwa server yang memproduksi halaman bahkan merupakan komputer yang sama yang menghosting beberapa sesi Server Interaktif nanti, termasuk untuk komponen WebAssembly di mana server adalah komputer yang berbeda dengan klien. Manfaat penyajian sisi server statis (SSR statis) adalah untuk mendapatkan performa penuh penyajian HTML stateless murni.
Status yang melintasi batas antara penyajian statis dan interaktif harus dapat diserialisasikan. Komponen adalah objek arbitrer yang mereferensikan rantai besar objek lain, termasuk perender, kontainer DI, dan setiap instans layanan DI. Anda harus secara eksplisit menyebabkan status diserialisasikan dari SSR statis agar tersedia dalam komponen yang dirender secara interaktif berikutnya. Dua pendekatan diadopsi:
- Blazor Melalui kerangka kerja, parameter yang diteruskan di seluruh SSR statis ke batas penyajian interaktif diserialisasikan secara otomatis jika dapat diserialisasikan JSON, atau kesalahan dilemparkan.
- Status yang disimpan dalam diserialisasikan dan dipulihkan
PersistentComponentState
secara otomatis jika dapat diserialisasikan JSON, atau kesalahan dilemparkan.
Parameter berjenjang tidak dapat diserialisasikan JSON karena pola penggunaan umum untuk parameter berjenjang agak seperti layanan DI. Seringkali ada varian parameter kaskading khusus platform, sehingga tidak akan membantu pengembang jika kerangka kerja menghentikan pengembang agar tidak memiliki versi khusus interaktif server atau versi khusus WebAssembly. Selain itu, banyak nilai parameter berjenjang secara umum tidak dapat diserialisasikan, sehingga tidak praktis untuk memperbarui aplikasi yang ada jika Anda harus berhenti menggunakan semua nilai parameter berjenjang yang tidak dapat diserialisasi.
Rekomendasi:
Jika Anda perlu membuat status tersedia untuk semua komponen interaktif sebagai parameter berskala, sebaiknya gunakan nilai kaskading tingkat akar. Pola pabrik tersedia, dan aplikasi dapat memancarkan nilai yang diperbarui setelah pengaktifan aplikasi. Nilai kaskading tingkat akar tersedia untuk semua komponen, termasuk komponen interaktif, karena diproses sebagai layanan DI.
Untuk penulis pustaka komponen, Anda dapat membuat metode ekstensi untuk konsumen pustaka yang mirip dengan yang berikut ini:
builder.Services.AddLibraryCascadingParameters();
Instruksikan pengembang untuk memanggil metode ekstensi Anda. Ini adalah alternatif yang baik untuk menginstruksikan mereka untuk menambahkan
<RootComponent>
komponen dalam komponennyaMainLayout
.
Kaskade beberapa nilai
Untuk menyimpan beberapa nilai dengan jenis yang sama dalam subtree yang sama, berikan string unik Name untuk setiap CascadingValue
komponen dan atribut yang [CascadingParameter]
sesuai.
Dalam contoh berikut, dua CascadingValue
komponen kaskade instans yang berbeda dari CascadingType
:
<CascadingValue Value="parentCascadeParameter1" Name="CascadeParam1">
<CascadingValue Value="ParentCascadeParameter2" Name="CascadeParam2">
...
</CascadingValue>
</CascadingValue>
@code {
private CascadingType? parentCascadeParameter1;
[Parameter]
public CascadingType? ParentCascadeParameter2 { get; set; }
}
Dalam komponen turunan, parameter berkaskade menerima nilai kaskadenya dari komponen leluhur dengan Name:
@code {
[CascadingParameter(Name = "CascadeParam1")]
protected CascadingType? ChildCascadeParameter1 { get; set; }
[CascadingParameter(Name = "CascadeParam2")]
protected CascadingType? ChildCascadeParameter2 { get; set; }
}
Meneruskan data di seluruh hierarki komponen
Parameter berskala juga memungkinkan komponen untuk meneruskan data di seluruh hierarki komponen. Pertimbangkan contoh set tab UI berikut, di mana komponen kumpulan tab mempertahankan serangkaian tab individual.
Catatan
Untuk contoh di bagian ini, namespace aplikasi adalah BlazorSample
. Saat bereksperimen dengan kode di aplikasi sampel Anda sendiri, ubah namespace ke namespace aplikasi sampel Anda.
Buat ITab
antarmuka yang diterapkan tab dalam folder bernama UIInterfaces
.
UIInterfaces/ITab.cs
:
using Microsoft.AspNetCore.Components;
namespace BlazorSample.UIInterfaces;
public interface ITab
{
RenderFragment ChildContent { get; }
}
Catatan
Untuk informasi selengkapnya tentang RenderFragment, lihat komponen ASP.NET CoreRazor.
Komponen berikut TabSet
mempertahankan sekumpulan tab. Komponen kumpulan Tab
tab, yang dibuat nanti di bagian ini, berikan item daftar (<li>...</li>
) untuk daftar (<ul>...</ul>
).
Komponen anak Tab
tidak secara eksplisit diteruskan sebagai parameter ke TabSet
. Sebaliknya, komponen anak Tab
adalah bagian dari konten anak dari TabSet
. Namun, TabSet
masih memerlukan referensi setiap Tab
komponen sehingga dapat merender header dan tab aktif. Untuk mengaktifkan koordinasi ini tanpa memerlukan kode tambahan, TabSet
komponen dapat menyediakan dirinya sebagai nilai bertingkat yang kemudian diambil oleh komponen keturunan Tab
.
TabSet.razor
:
@using BlazorSample.UIInterfaces
<!-- Display the tab headers -->
<CascadingValue Value="this">
<ul class="nav nav-tabs">
@ChildContent
</ul>
</CascadingValue>
<!-- Display body for only the active tab -->
<div class="nav-tabs-body p-4">
@ActiveTab?.ChildContent
</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
public ITab? ActiveTab { get; private set; }
public void AddTab(ITab tab)
{
if (ActiveTab is null)
{
SetActiveTab(tab);
}
}
public void SetActiveTab(ITab tab)
{
if (ActiveTab != tab)
{
ActiveTab = tab;
StateHasChanged();
}
}
}
Tab
Komponen turunan menangkap yang berisi TabSet
sebagai parameter berskala. Komponen Tab
menambahkan diri mereka ke TabSet
dan berkoordinasi untuk mengatur tab aktif.
Tab.razor
:
@using BlazorSample.UIInterfaces
@implements ITab
<li>
<a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
@Title
</a>
</li>
@code {
[CascadingParameter]
public TabSet? ContainerTabSet { get; set; }
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
private string? TitleCssClass =>
ContainerTabSet?.ActiveTab == this ? "active" : null;
protected override void OnInitialized()
{
ContainerTabSet?.AddTab(this);
}
private void ActivateTab()
{
ContainerTabSet?.SetActiveTab(this);
}
}
Komponen berikut ExampleTabSet
menggunakan TabSet
komponen , yang berisi tiga Tab
komponen.
ExampleTabSet.razor
:
@page "/example-tab-set"
<TabSet>
<Tab Title="First tab">
<h4>Greetings from the first tab!</h4>
<label>
<input type="checkbox" @bind="showThirdTab" />
Toggle third tab
</label>
</Tab>
<Tab Title="Second tab">
<h4>Hello from the second tab!</h4>
</Tab>
@if (showThirdTab)
{
<Tab Title="Third tab">
<h4>Welcome to the disappearing third tab!</h4>
<p>Toggle this tab from the first tab.</p>
</Tab>
}
</TabSet>
@code {
private bool showThirdTab;
}
Sumber Daya Tambahan:
ASP.NET Core