# Blazor Common Pitfalls & Solutions

> **Scope:** blazor-azure
> **Layer:** 2 (on keyword)
> **Keywords:** blazor, dbcontext, jsruntime, rendermode, pitfalls, errors
> **Load When:** blazor work

Production-tested pitfalls from real Blazor Server projects.

> **Ref:** `blazor-lifecycle.md`, `blazor-state.md`, `html-to-blazor.md`

---

## CRITICAL: DI Pattern (Component → API → Handler)

| Wrong | Right |
|-------|-------|
| `[Inject] IProcessCommand Command` | `[Inject] HttpClient Http` |
| Direct Application layer injection | HttpClient → Controller → MediatR → Handler |

```csharp
// Component
[Inject] HttpClient Http { get; set; }
[Inject] NavigationManager Nav { get; set; }

private async Task HandleUpload()
{
    var uri = new Uri(new Uri(Nav.BaseUri), "/api/upload");
    var response = await Http.PostAsync(uri, content);
    if (response.IsSuccessStatusCode)
        Nav.NavigateTo($"/processing/{await response.Content.ReadFromJsonAsync<UploadResponse>().JobId}");
}

// Program.cs
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped(sp => {
    var client = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
    var req = sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.Request;
    if (req != null) client.BaseAddress = new Uri($"{req.Scheme}://{req.Host}");
    return client;
});
```

---

## Common Configuration Errors

| Issue | Fix |
|-------|-----|
| CSS loads but doesn't apply | Add `app.UseStaticFiles()` BEFORE routing |
| "Cannot pass Body parameter" | Remove `@rendermode` from layouts (only on pages) |
| JSRuntime crashes | Use in `OnAfterRenderAsync(firstRender)`, NOT `OnInitializedAsync` |

```csharp
// Correct middleware order
app.UseHttpsRedirection();
app.UseStaticFiles();  // BEFORE routing
app.UseAntiforgery();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
```

---

## File Upload Patterns

### IBrowserFile: Read Once, Store Bytes

`IBrowserFile` loses reference after re-render. Always read immediately and store bytes.

```csharp
private byte[]? _fileBytes;
private string? _previewUrl;

private async Task HandleFile(InputFileChangeEventArgs e)
{
    var file = e.File;
    var ext = Path.GetExtension(file.Name).ToLowerInvariant();
    if (!new[] { ".jpg", ".png" }.Contains(ext)) return;
    if (file.Size > 10_000_000) return; // 10MB

    _fileBytes = new byte[file.Size];
    await file.OpenReadStream(10_000_000).ReadExactlyAsync(_fileBytes);
    _previewUrl = $"data:{file.ContentType};base64,{Convert.ToBase64String(_fileBytes)}";
}

private async Task Upload()
{
    using var stream = new MemoryStream(_fileBytes!);
    var content = new StreamContent(stream);
    await Http.PostAsync("/api/upload", content);
}
```

**Critical:** Blazor default max = 512KB. Always specify `maxAllowedSize`.

### Stream-based Contract (IBrowserFile ↔ IFormFile)

```csharp
// Universal contract
public record UploadRequest(Stream Stream, string FileName, string ContentType);

// Blazor: file.OpenReadStream(maxSize)
// API: formFile.OpenReadStream()
```

---

## Razor Syntax Errors

| Error | Wrong | Right |
|-------|-------|-------|
| RZ9986 | `Style="animation-delay: @(0.05 * i)s;"` | `Style="@($"animation-delay: {0.05 * i}s;")"` |
| CS9034 | `required int Percent => ...` | `int Percent => ...` (no required on computed) |
| CS0104 | Duplicate enums in Domain + Application | Single enum in Domain, `using` elsewhere |

---

## JS Interop

```razor
<div @onclick="TriggerFileInput">Click to upload</div>
<InputFile id="fileInput" style="display:none" OnChange="HandleFile" />

@code {
    [Inject] IJSRuntime JS { get; set; }
    async Task TriggerFileInput() => await JS.InvokeVoidAsync("eval", "document.getElementById('fileInput').click()");
}
```

---

## Quick Debugging

| Symptom | Check |
|---------|-------|
| CSS not applying | `UseStaticFiles()` present? Hard refresh (Ctrl+Shift+R) |
| Component not interactive | `@rendermode InteractiveServer` on page (NOT layout) |
| JSRuntime crash | Called in `OnAfterRenderAsync(firstRender)` |
| DI "cannot provide value" | HttpClient pattern configured? Service registered? |
| State lost on navigation | `blazor-state.md` Persist+Reload pattern |

---

*MORPH-SPEC by Polymorphism Tech*

---

## Code Review Checklist

Use this checklist during code review for common Blazor pitfalls:

### Fluent UI / MudBlazor Components

#### FluentDialog
- [ ] **`Hidden="true"` explicitly set on FluentDialog?**
- [ ] **Modal state controlled by bool variable, not just dialog ref?**

#### Icons
- [ ] **Icons use correct size classes? (Size20, Size24, Size28, Size32, Size48)**
  - Size12, Size14, Size16 do NOT exist!
- [ ] **Icon namespace alias used?** (`@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons`)
- [ ] **Icon syntax is correct?** (`<FluentIcon Value="@(new Icons.Regular.Size24.Home())" />`)

#### Toast Service
- [ ] **Toast methods are synchronous (not async)?**
  - ✅ `ToastService.ShowSuccess("Message")`
  - ❌ `await ToastService.ShowSuccessAsync("Message")`

### Razor Syntax

#### String Interpolation
- [ ] **Complex attribute interpolation uses `@($"...")`?**
  - ✅ `<div style="@($"animation-delay: {delay}s;")">`
  - ❌ `<div style="animation-delay: @(delay)s;">` (causes RZ9986)

### Error Handling
- [ ] **Try-catch around async operations with user feedback?**
- [ ] **Loading state shown during async operations?**
- [ ] **Error messages are user-friendly, not stack traces?**

### Security
- [ ] **Input validation on all user inputs?**
- [ ] **No SQL injection vulnerabilities (using parameterized queries)?**

### Quick Reference: Common Mistakes

| Mistake | Fix |
|---------|-----|
| JSRuntime in OnInitializedAsync | Move to OnAfterRenderAsync |
| Reading IBrowserFile twice | Store bytes on first read |
| Scoped DbContext in Task.Run | Use IDbContextFactory |
| forceLoad navigation | Use normal navigation + persist state |
| ToastService.ShowSuccessAsync | Use synchronous ShowSuccess |

---

*MORPH-SPEC by Polymorphism Tech*
