Files
Antigravity 5a2223a4c8 feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)
This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility.

### Key Changes
- **Infrastructure Stabilization**:
  - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support.
  - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35).
  - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37).
- **WASM Client Functional Proxies**:
  - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`.
  - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`.
- **Domain Refinement**:
  - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states.

### Related Issues
- Fixes #35
- Fixes #36
- Fixes #37

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #42
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-05-13 18:24:24 +00:00

12 KiB

Blazor Components & Component Lifecycle

Component Structure

Components are the fundamental building blocks of Blazor applications. A Blazor component is a self-contained piece of UI with an optional logic.

Basic Component Syntax

@page "/example"
@using MyApp.Services
@inject IMyService MyService

<h3>@Title</h3>
<div>@ChildContent</div>
<button @onclick="HandleClick">Click me</button>

@code {
    [Parameter]
    public string Title { get; set; } = "Default";

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

    private void HandleClick()
    {
        // Handle button click
    }
}

Key elements:

  • @page directive: Makes component routable (optional for non-page components)
  • @using: Import namespaces
  • @inject: Dependency injection
  • HTML markup: Regular HTML with Blazor directives
  • @code block: C# logic including lifecycle methods

Component vs Page

  • Page Component: Has @page directive, routable via URL

    • Example: /Counter route
    • Located in Pages/ folder (convention)
  • Reusable Component: No @page directive, used by other components

    • Example: <UserCard @bind-User="user" />
    • Located in Shared/ or domain-specific folder

Component Lifecycle

Lifecycle Sequence

Component lifecycle methods execute in this order:

1. SetParametersAsync()
   ↓
2. OnInitialized() or OnInitializedAsync()
   ↓
3. OnParametersSet() or OnParametersSetAsync()
   ↓
4. ShouldRender() [decision point - skip if returns false]
   ↓
5. OnAfterRender() or OnAfterRenderAsync()

When parameters change (parent re-renders):

SetParametersAsync() [parameters updated]
  ↓
OnParametersSet() [NOT OnInitialized - that runs once only]
  ↓
ShouldRender()
  ↓
OnAfterRender()

Lifecycle Methods Detailed

SetParametersAsync()

  • When: First method called, before initialization
  • Purpose: Set component parameters
  • Usage: Rarely overridden, use OnInitialized instead
  • Code Example:
public override async Task SetParametersAsync(ParameterView parameters)
{
    // Custom parameter processing if needed
    await base.SetParametersAsync(parameters);
}

OnInitialized / OnInitializedAsync()

  • When: Once per component lifetime, after parameters set
  • Purpose: Initialize component state, load data
  • Runs: Only ONCE, even if parameters change
  • Code Example:
protected override async Task OnInitializedAsync()
{
    await base.OnInitializedAsync();
    data = await Service.LoadDataAsync();
}

Common Uses:

  • Load initial data from API
  • Set up subscriptions
  • Initialize state based on parameters

OnParametersSet / OnParametersSetAsync()

  • When: After parameters set, runs EVERY time parameters change
  • Purpose: React to parameter changes
  • Runs: Every time parent re-renders with different values
  • Code Example:
protected override async Task OnParametersSetAsync()
{
    await base.OnParametersSetAsync();
    if (UserId != previousUserId)
    {
        data = await Service.LoadUserDataAsync(UserId);
        previousUserId = UserId;
    }
}

Common Uses:

  • Update UI based on new parameter values
  • Fetch new data when ID parameter changes
  • React to cascading parameter changes

ShouldRender()

  • When: Before DOM rendering, decision point
  • Purpose: Optimize rendering by skipping unnecessary renders
  • Returns: true (render) or false (skip)
  • Code Example:
protected override bool ShouldRender()
{
    // Only render if specific field changed
    return hasChanged;
}

Common Optimizations:

  • Skip render if data unchanged
  • Prevent re-render from external events
  • Implement custom change detection

OnAfterRender / OnAfterRenderAsync()

  • When: After component rendered to DOM
  • Purpose: Work with DOM, initialize JS libraries, final setup
  • Parameter: firstRender - true only on first render
  • Code Example:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // Initialize JS library only once
        await JS.InvokeVoidAsync("initializeChart", elementRef);
    }
}

Critical Use Case:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    
    if (firstRender)
    {
        // Load JS module
        module = await JS.InvokeAsync<IJSObjectReference>(
            "import", "./scripts/myScript.js");
        
        // Initialize library
        await module.InvokeVoidAsync("setupChart", element);
    }
}

Important: Always use firstRender check for one-time initialization. This prevents re-initializing on every parameter change.

Component Parameters

Parameter Declaration

@code {
    // Simple parameter
    [Parameter]
    public string Title { get; set; } = "Default";

    // Required parameter (C# 11+)
    [Parameter, EditorRequired]
    public int UserId { get; set; }

    // Child content
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    // Event callback
    [Parameter]
    public EventCallback<string> OnValueChanged { get; set; }

    // Cascading parameter
    [CascadingParameter]
    public ThemeInfo? CurrentTheme { get; set; }
}

Parameter Best Practices

Use Clear Names:

// ✅ Good - clear intent
[Parameter]
public bool IsVisible { get; set; }

// ❌ Poor - ambiguous
[Parameter]
public bool State { get; set; }

Use Nullable Types for Optional:

// ✅ Good - nullable indicates optional
[Parameter]
public string? OptionalValue { get; set; }

// ✅ Good - default value
[Parameter]
public int MaxItems { get; set; } = 10;

// ❌ Poor - not clear if optional
[Parameter]
public string RequiredValue { get; set; }

Use [EditorRequired] for Required Parameters (C# 11+):

// ✅ Best practice - compiler enforces, IDE warns
[Parameter, EditorRequired]
public string Title { get; set; } = default!;

// Fallback for older C#
[Parameter]
public string Title { get; set; } = default!;

Use EventCallback for Child-to-Parent Communication:

// ✅ Correct - EventCallback for async safety
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }

// ✅ With custom args
[Parameter]
public EventCallback<ValueChangeEventArgs> OnValueChanged { get; set; }

// ❌ Avoid - direct Action, not async-safe
[Parameter]
public Action<string>? OnValueChanged { get; set; }

Parameter Change Detection

To know when a parameter changed:

@code {
    private int previousUserId;

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

    protected override async Task OnParametersSetAsync()
    {
        if (UserId != previousUserId)
        {
            previousUserId = UserId;
            await LoadUserData();
        }
    }
}

Or use a comparison strategy:

private object? previousCriteria;

protected override async Task OnParametersSetAsync()
{
    var currentCriteria = (SearchId, SearchTerm);
    
    if (!Equals(previousCriteria, currentCriteria))
    {
        previousCriteria = currentCriteria;
        await PerformSearch();
    }
}

Cascading Values

Cascading values allow ancestor components to provide data to all descendants without explicit parameter passing.

Providing Cascading Values

<!-- Parent component -->
<CascadingValue Value="@currentUser">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
    
    private User currentUser = new();
}

Receiving Cascading Values

<!-- Child component anywhere in hierarchy -->
@code {
    [CascadingParameter]
    public User? CurrentUser { get; set; }
    
    protected override void OnInitialized()
    {
        if (CurrentUser == null)
        {
            // Handle missing cascading value
        }
    }
}

Multiple Cascading Values

<!-- Provider -->
<CascadingValue Value="@theme">
    <CascadingValue Value="@currentUser">
        <CascadingValue Value="@permissions">
            @ChildContent
        </CascadingValue>
    </CascadingValue>
</CascadingValue>

<!-- Consumer - multiple parameters -->
@code {
    [CascadingParameter]
    public Theme? Theme { get; set; }
    
    [CascadingParameter]
    public User? CurrentUser { get; set; }
    
    [CascadingParameter]
    public Permissions? Permissions { get; set; }
}

Named Cascading Values

For disambiguation when multiple values of same type:

<!-- Provider -->
<CascadingValue Value="@themeLight" Name="Light">
    <CascadingValue Value="@themeDark" Name="Dark">
        @ChildContent
    </CascadingValue>
</CascadingValue>

<!-- Consumer -->
@code {
    [CascadingParameter(Name = "Light")]
    public Theme? LightTheme { get; set; }
    
    [CascadingParameter(Name = "Dark")]
    public Theme? DarkTheme { get; set; }
}

RenderFragment for Component Composition

RenderFragment enables flexible component composition.

Basic RenderFragment

<!-- Parent component -->
<div>
    <h2>Header</h2>
    @ChildContent
    <footer>Footer</footer>
</div>

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

<!-- Usage -->
<Layout>
    <p>This is the main content</p>
</Layout>

Typed RenderFragment with Context

<!-- ListComponent.razor -->
@foreach (var item in Items)
{
    @ItemTemplate(item)
}

@code {
    [Parameter]
    public IEnumerable<Item> Items { get; set; } = [];
    
    [Parameter]
    public RenderFragment<Item>? ItemTemplate { get; set; }
}

<!-- Usage -->
<ListComponent Items="@items">
    <ItemTemplate Context="item">
        <div>@item.Name - @item.Price</div>
    </ItemTemplate>
</ListComponent>

Multiple Named Content Areas

<!-- Card component with multiple slots -->
<div class="card">
    <div class="card-header">@Header</div>
    <div class="card-body">@Body</div>
    <div class="card-footer">@Footer</div>
</div>

@code {
    [Parameter]
    public RenderFragment? Header { get; set; }
    
    [Parameter]
    public RenderFragment? Body { get; set; }
    
    [Parameter]
    public RenderFragment? Footer { get; set; }
}

<!-- Usage -->
<Card>
    <Header>
        <h3>Card Title</h3>
    </Header>
    <Body>
        <p>Card content</p>
    </Body>
    <Footer>
        <button>Action</button>
    </Footer>
</Card>

Component Best Practices

Single Responsibility

  • Each component should have one clear purpose
  • Avoid god components that do too much
  • Example: UserProfile component should focus on displaying user info, not handle complex business logic

Composition Over Inheritance

  • Use cascading values for shared state, not deep hierarchies
  • Compose components rather than creating base classes
  • Example: Create theme provider component instead of theme-aware base class

Keep Components Simple

  • Minimize @code block logic
  • Move complex logic to services
  • Example: Validation logic → ValidationService, not in component

Proper Disposal

  • Implement IDisposable or IAsyncDisposable
  • Unsubscribe from events
  • Dispose timers and resources
@implements IAsyncDisposable
@inject IJSRuntime JS

private IJSObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await JS.InvokeAsync<IJSObjectReference>(
            "import", "./myScript.js");
    }
}

async ValueTask IAsyncDisposable.DisposeAsync()
{
    if (module is not null)
    {
        await module.DisposeAsync();
    }
}

Related Resources: See state-management-events.md for event handling and state updates. See performance-advanced.md for optimization techniques.