C# Blazor Best Practices ที่ต้องรู้ — พัฒนา Web
Blazor Best Practices
C# Blazor Best Practices Component Design State Management Performance Authentication Testing Blazor Server WebAssembly Hybrid .NET 8 Production
| Mode | Execution | Latency | Offline | SEO | เหมาะกับ |
|---|---|---|---|---|---|
| Blazor Server | Server (SignalR) | ต่ำ | ไม่ได้ | ดี (SSR) | Internal apps, real-time |
| Blazor WASM | Browser (WebAssembly) | สูง (initial) | ได้ (PWA) | ต้อง Pre-render | SPA, offline apps |
| Blazor Auto | Server → WASM | ต่ำ → ต่ำ | ได้ (หลัง switch) | ดี | .NET 8 recommended |
| Blazor Hybrid | Native (MAUI) | ต่ำมาก | ได้ | N/A | Desktop/Mobile app |
Component Patterns
=== Blazor Component Patterns ===
ProductCard.razor — Single Responsibility Component
@code {
[Parameter] public ProductDto Product { get; set; } = null!;
[Parameter] public EventCallback<int> OnAddToCart { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
private async Task HandleAddToCart()
{
await OnAddToCart.InvokeAsync(Product.Id);
}
}
GenericList.razor — Template Component
@typeparam TItem
@code {
[Parameter] public IEnumerable<TItem> Items { get; set; } = [];
[Parameter] public RenderFragment<TItem> ItemTemplate { get; set; } = null!;
[Parameter] public RenderFragment? EmptyTemplate { get; set; }
}
<div>
@if (Items.Any())
{
@foreach (var item in Items)
{
@ItemTemplate(item)
}
}
else
{
@EmptyTemplate
}
</div>
Usage:
<GenericList Items="products" Context="product">
<ItemTemplate>
<ProductCard Product="product" OnAddToCart="AddToCart" />
</ItemTemplate>
<EmptyTemplate>
<p>No products found</p>
</EmptyTemplate>
</GenericList>
from dataclasses import dataclass
@dataclass
class ComponentPattern:
pattern: str
use_case: str
example: str
benefit: str
patterns = [
ComponentPattern("Parameter + EventCallback", "Parent-child communication",
"[Parameter] public string Title { get; set; }", "Clear data flow"),
ComponentPattern("CascadingValue", "Deep component tree data",
"<CascadingValue Value='theme'>", "Avoid prop drilling"),
ComponentPattern("RenderFragment", "Template/slot components",
"[Parameter] public RenderFragment ChildContent", "Flexible composition"),
ComponentPattern("@typeparam", "Generic reusable components",
"@typeparam TItem on GenericList", "Type-safe reusable lists"),
ComponentPattern("@key", "Efficient list rendering",
"@foreach with @key='item.Id'", "Prevent full list re-render"),
ComponentPattern("Base Component", "Shared logic inheritance",
"ComponentBase with common methods", "DRY principle"),
]
print("=== Component Patterns ===")
for p in patterns:
print(f" [{p.pattern}] Use: {p.use_case}")
print(f" Example: {p.example}")
print(f" Benefit: {p.benefit}")
State and Performance
=== State Management and Performance ===
State Container Pattern
public class CartState
{
public List<CartItem> Items { get; private set; } = [];
public decimal Total => Items.Sum(i => i.Price * i.Quantity);
public event Action? OnChange;
public void AddItem(CartItem item)
{
var existing = Items.FirstOrDefault(i => i.ProductId == item.ProductId);
if (existing != null)
existing.Quantity += item.Quantity;
else
Items.Add(item);
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
Register in DI
builder.Services.AddScoped<CartState>();
Use in Component
@inject CartState Cart
@implements IDisposable
protected override void OnInitialized()
{
Cart.OnChange += StateHasChanged;
}
public void Dispose()
{
Cart.OnChange -= StateHasChanged;
}
Performance: Virtualization
<Virtualize Items="products" Context="product">
<ProductCard Product="product" />
</Virtualize>
Performance: ShouldRender override
protected override bool ShouldRender()
{
return _hasChanged;
}
@dataclass
class PerfTip:
tip: str
problem: str
solution: str
impact: str
tips = [
PerfTip("Use @key", "List re-renders all items on change",
"@key='item.Id' on each item", "50-90% fewer DOM updates"),
PerfTip("Virtualize", "Rendering 1000+ items causes lag",
"<Virtualize Items='list'> renders only visible", "Instant scroll for large lists"),
PerfTip("ShouldRender", "Component re-renders unnecessarily",
"Override ShouldRender() return false when no change", "Eliminate wasted renders"),
PerfTip("Dispose handlers", "Event handlers cause memory leaks",
"Implement IDisposable, unsubscribe in Dispose()", "Prevent memory growth"),
PerfTip("Lazy Assembly", "WASM downloads all DLLs upfront",
"LazyAssemblyLoader for rarely-used pages", "Reduce initial download 30-60%"),
PerfTip("Pre-render SSR", "WASM shows blank before JS loads",
"Use InteractiveAuto for SSR + WASM", "Instant first paint"),
]
print("\n=== Performance Tips ===")
for t in tips:
print(f" [{t.tip}] Problem: {t.problem}")
print(f" Fix: {t.solution}")
print(f" Impact: {t.impact}")
Auth and Testing
=== Authentication and Testing ===
Authentication Setup (.NET 8)
builder.Services.AddAuthentication()
.AddCookie()
.AddJwtBearer(options => {
options.TokenValidationParameters = new() {
ValidateIssuer = true,
ValidIssuer = "https://myapp.com",
ValidateAudience = true,
ValidAudience = "myapp-api",
IssuerSigningKey = new SymmetricSecurityKey(key),
};
});
AuthorizeView in Razor
<AuthorizeView Roles="Admin">
<Authorized>
<AdminPanel />
</Authorized>
<NotAuthorized>
<p>Access Denied</p>
</NotAuthorized>
</AuthorizeView>
[Authorize] on page
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
bUnit Testing
[Fact]
public void ProductCard_ShowsName()
{
using var ctx = new TestContext();
var product = new ProductDto { Id = 1, Name = "Test", Price = 99 };
var cut = ctx.RenderComponent<ProductCard>(
parameters => parameters.Add(p => p.Product, product));
cut.Find("h3").TextContent.ShouldBe("Test");
}
[Fact]
public void AddToCart_FiresCallback()
{
using var ctx = new TestContext();
var clicked = false;
var cut = ctx.RenderComponent<ProductCard>(parameters => parameters
.Add(p => p.Product, product)
.Add(p => p.OnAddToCart, EventCallback.Factory.Create<int>(this, id => clicked = true)));
cut.Find("button").Click();
clicked.ShouldBeTrue();
}
@dataclass
class BestPractice:
category: str
practice: str
why: str
practices = [
BestPractice("Component", "แยก Component เล็ก ทำหน้าที่เดียว", "Reusable, testable, maintainable"),
BestPractice("State", "ใช้ State Container Pattern + DI", "Centralized state, predictable updates"),
BestPractice("Performance", "ใช้ @key Virtualize ShouldRender", "Smooth UI, less memory"),
BestPractice("Auth", "Server-side validation เสมอ ไม่พึ่ง UI", "Security cannot be client-side only"),
BestPractice("Testing", "ใช้ bUnit test ทุก Component", "Catch bugs early, safe refactor"),
BestPractice("Error", "ใช้ ErrorBoundary wrap components", "Graceful error handling"),
BestPractice("DI", "Register services ด้วย Scoped lifetime", "Correct lifecycle for Blazor"),
BestPractice("Dispose", "Implement IDisposable ทุก Component ที่ subscribe", "Prevent memory leaks"),
]
print("Best Practices:")
for b in practices:
print(f" [{b.category}] {b.practice}")
print(f" Why: {b.why}")
เคล็ดลับ
- @key: ใช้ @key ทุก List Rendering ป้องกัน Re-render ทั้งหมด
- Dispose: Implement IDisposable ทุก Component ที่ Subscribe Event
- Auto Mode: ใช้ InteractiveAuto ใน .NET 8 ได้ SSR + WASM อัตโนมัติ
- bUnit: เขียน bUnit Test ทุก Component ตรวจ Render + Events
- ErrorBoundary: Wrap Component ด้วย ErrorBoundary ป้องกัน Crash
Blazor คืออะไร
Web Framework Microsoft C# แทน JavaScript Interactive Web App Server SignalR WebAssembly Browser Hybrid MAUI Razor Component .NET 8 Auto Render