SiamCafe · Blog
C# Blazor Best Practices ที่ต้องรู้ — พัฒนา Web
บทความ

C# Blazor Best Practices ที่ต้องรู้ — พัฒนา Web

เผยแพร่ 28 พฤษภาคม 2569

Blazor Best Practices

C# Blazor Best Practices Component Design State Management Performance Authentication Testing Blazor Server WebAssembly Hybrid .NET 8 Production

ModeExecutionLatencyOfflineSEOเหมาะกับ
Blazor ServerServer (SignalR)ต่ำไม่ได้ดี (SSR)Internal apps, real-time
Blazor WASMBrowser (WebAssembly)สูง (initial)ได้ (PWA)ต้อง Pre-renderSPA, offline apps
Blazor AutoServer → WASMต่ำ → ต่ำได้ (หลัง switch)ดี.NET 8 recommended
Blazor HybridNative (MAUI)ต่ำมากได้N/ADesktop/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&lt;CartItem&gt; 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&lt;CartState&gt;();

Use in Component

@inject CartState Cart

@implements IDisposable

protected override void OnInitialized()

{

Cart.OnChange += StateHasChanged;

}

public void Dispose()

{

Cart.OnChange -= StateHasChanged;

}

Performance: Virtualization

&lt;Virtualize Items="products" Context="product"&gt;

&lt;ProductCard Product="product" /&gt;

&lt;/Virtualize&gt;

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

&lt;AuthorizeView Roles="Admin"&gt;

&lt;Authorized&gt;

&lt;AdminPanel /&gt;

&lt;/Authorized&gt;

&lt;NotAuthorized&gt;

&lt;p&gt;Access Denied&lt;/p&gt;

&lt;/NotAuthorized&gt;

&lt;/AuthorizeView&gt;

[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&lt;ProductCard&gt;(

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&lt;ProductCard&gt;(parameters => parameters

.Add(p => p.Product, product)

.Add(p => p.OnAddToCart, EventCallback.Factory.Create&lt;int&gt;(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