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>
This commit was merged in pull request #42.
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
# 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
|
||||
|
||||
```csharp
|
||||
@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**:
|
||||
|
||||
```csharp
|
||||
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**:
|
||||
|
||||
```csharp
|
||||
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**:
|
||||
|
||||
```csharp
|
||||
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**:
|
||||
|
||||
```csharp
|
||||
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**:
|
||||
|
||||
```csharp
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Initialize JS library only once
|
||||
await JS.InvokeVoidAsync("initializeChart", elementRef);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Use Case:**
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
@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:**
|
||||
|
||||
```csharp
|
||||
// ✅ Good - clear intent
|
||||
[Parameter]
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
// ❌ Poor - ambiguous
|
||||
[Parameter]
|
||||
public bool State { get; set; }
|
||||
```
|
||||
|
||||
**Use Nullable Types for Optional:**
|
||||
|
||||
```csharp
|
||||
// ✅ 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+):**
|
||||
|
||||
```csharp
|
||||
// ✅ 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:**
|
||||
|
||||
```csharp
|
||||
// ✅ 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:
|
||||
|
||||
```csharp
|
||||
@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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
<!-- Parent component -->
|
||||
<CascadingValue Value="@currentUser">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private User currentUser = new();
|
||||
}
|
||||
```
|
||||
|
||||
### Receiving Cascading Values
|
||||
|
||||
```csharp
|
||||
<!-- 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
|
||||
|
||||
```csharp
|
||||
<!-- 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:
|
||||
|
||||
```csharp
|
||||
<!-- 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
|
||||
|
||||
```csharp
|
||||
<!-- 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
|
||||
|
||||
```csharp
|
||||
<!-- 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
|
||||
|
||||
```csharp
|
||||
<!-- 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
|
||||
|
||||
```csharp
|
||||
@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](state-management-events.md) for event handling and state updates. See [performance-advanced.md](performance-advanced.md) for optimization techniques.
|
||||
Reference in New Issue
Block a user