# Blazor Fusion (Stl.Fusion) ## Overview Stl.Fusion is a library that brings computed observables and real-time state synchronization to .NET applications, with first-class support for Blazor. It introduces `[ComputeMethod]` -- methods whose outputs are cached and automatically invalidated when their dependencies change. When a compute method's result changes, all Blazor components consuming that result re-render automatically. Fusion handles server-to-client replication transparently, making Blazor Server and Blazor WebAssembly apps display real-time data without manual SignalR hubs, polling, or state management boilerplate. Fusion's core abstraction is `IComputed`, a versioned, cached container for a method's return value. Computed instances form a dependency graph -- when an upstream computed value is invalidated, all downstream dependents are also invalidated and lazily recomputed on next access. ## Installation ```bash dotnet add package Stl.Fusion dotnet add package Stl.Fusion.Blazor dotnet add package Stl.Fusion.Server # For Blazor Server or API hosting ``` ## Defining Compute Services Compute services are interfaces with `[ComputeMethod]` attributes. Implementations must be registered as singletons because Fusion caches results per-instance. ```csharp using Stl.Fusion; namespace MyApp.Services; public interface IProductService : IComputeService { [ComputeMethod] Task> GetAllAsync(CancellationToken cancellationToken = default); [ComputeMethod] Task GetByIdAsync(int id, CancellationToken cancellationToken = default); [ComputeMethod] Task GetCountAsync(CancellationToken cancellationToken = default); Task AddAsync(Product product, CancellationToken cancellationToken = default); Task UpdateAsync(Product product, CancellationToken cancellationToken = default); } public record Product(int Id, string Name, decimal Price, int StockCount); ``` ```csharp using Stl.Fusion; namespace MyApp.Services; public class ProductService : IProductService { private readonly List _products = new(); private int _nextId = 1; [ComputeMethod] public virtual async Task> GetAllAsync( CancellationToken cancellationToken = default) { return _products.ToList(); } [ComputeMethod] public virtual async Task GetByIdAsync( int id, CancellationToken cancellationToken = default) { return _products.FirstOrDefault(p => p.Id == id); } [ComputeMethod] public virtual async Task GetCountAsync( CancellationToken cancellationToken = default) { return _products.Count; } public virtual async Task AddAsync( Product product, CancellationToken cancellationToken = default) { var newProduct = product with { Id = _nextId++ }; _products.Add(newProduct); // Invalidate computed values that depend on the product list using (Computed.Invalidate()) { _ = GetAllAsync(cancellationToken); _ = GetCountAsync(cancellationToken); } } public virtual async Task UpdateAsync( Product product, CancellationToken cancellationToken = default) { var index = _products.FindIndex(p => p.Id == product.Id); if (index >= 0) { _products[index] = product; using (Computed.Invalidate()) { _ = GetAllAsync(cancellationToken); _ = GetByIdAsync(product.Id, cancellationToken); } } } } ``` ## Blazor Component Integration Fusion provides `ComputedStateComponent` as a base class that automatically subscribes to computed values and re-renders when they change. ```csharp @page "/products" @using Stl.Fusion.Blazor @using MyApp.Services @inherits ComputedStateComponent>

Products (@State.Value?.Count ?? 0)

@if (State.HasValue) { @foreach (var product in State.Value) { }
Name Price Stock
@product.Name @product.Price.ToString("C") @product.StockCount
} else if (State.Error is not null) {
Error: @State.Error.Message
} else {
Loading...
} @code { [Inject] private IProductService ProductService { get; set; } = default!; protected override async Task> ComputeState(CancellationToken cancellationToken) { return await ProductService.GetAllAsync(cancellationToken); } } ``` ## Invalidation Patterns Fusion uses explicit invalidation. When data changes, you invalidate the affected compute methods and Fusion propagates the change through the dependency graph. ```csharp using Stl.Fusion; namespace MyApp.Services; public class OrderService : IOrderService { private readonly IProductService _productService; public OrderService(IProductService productService) { _productService = productService; } // This compute method depends on GetByIdAsync -- Fusion tracks the dependency [ComputeMethod] public virtual async Task GetOrderTotalAsync( int[] productIds, CancellationToken cancellationToken = default) { decimal total = 0; foreach (int id in productIds) { var product = await _productService.GetByIdAsync(id, cancellationToken); if (product is not null) total += product.Price; } return total; } public virtual async Task PlaceOrderAsync( Order order, CancellationToken cancellationToken = default) { // Process order... // When product stock changes, invalidate the product foreach (var item in order.Items) { var product = await _productService.GetByIdAsync( item.ProductId, cancellationToken); if (product is not null) { var updated = product with { StockCount = product.StockCount - item.Quantity }; await _productService.UpdateAsync(updated, cancellationToken); } } } } public record Order(int Id, OrderItem[] Items); public record OrderItem(int ProductId, int Quantity); public interface IOrderService : IComputeService { [ComputeMethod] Task GetOrderTotalAsync( int[] productIds, CancellationToken cancellationToken = default); Task PlaceOrderAsync( Order order, CancellationToken cancellationToken = default); } ``` ## Service Registration ```csharp // Program.cs using Stl.Fusion; using MyApp.Services; var builder = WebApplication.CreateBuilder(args); var fusion = builder.Services.AddFusion(); fusion.AddService(); fusion.AddService(); fusion.AddBlazor(); // Adds Blazor-specific Fusion services builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); var app = builder.Build(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); app.Run(); ``` ## Computed State Options Configure update behavior on components. ```csharp @inherits ComputedStateComponent @code { [Inject] private IDashboardService DashboardService { get; set; } = default!; protected override ComputedState.Options GetStateOptions() { return new() { // How often to check for updates UpdateDelayer = FixedDelayer.Get(TimeSpan.FromSeconds(1)), // Initial value before first computation InitialValue = new DashboardData(0, 0, 0m) }; } protected override async Task ComputeState( CancellationToken cancellationToken) { return await DashboardService.GetDashboardAsync(cancellationToken); } } ``` ## Fusion vs. Traditional Blazor State Management | Feature | Stl.Fusion | Fluxor / Redux | Cascading Values | |--------------------------|-------------------------------------|----------------------------------|-------------------------| | Real-time updates | Automatic via invalidation | Manual dispatch required | Manual parameter flow | | Caching | Built-in computed cache | Manual memoization | None | | Dependency tracking | Automatic (call graph) | Manual selector composition | None | | Server-client sync | Transparent replication | Not built-in | Not built-in | | Boilerplate | `ComputeMethod` + `Invalidate` | Actions, reducers, effects | Properties, callbacks | | Learning curve | Moderate (invalidation model) | High (Redux concepts) | Low | | Multi-user broadcast | Built-in | Requires SignalR | Not applicable | ## Best Practices 1. **Make compute method implementations `virtual`** because Fusion uses Castle.DynamicProxy to intercept method calls and manage the computed cache; non-virtual methods bypass the proxy and produce stale data. 2. **Always invalidate compute methods inside a `using (Computed.Invalidate())` block** and call the same method signatures that need to be refreshed; Fusion matches invalidation targets by method identity and argument values. 3. **Register compute services as singletons** because Fusion caches computed results per-service-instance; transient or scoped registrations create new instances that bypass the cache and invalidation graph. 4. **Inherit from `ComputedStateComponent` for Blazor components that consume compute methods** rather than calling compute methods directly in `OnInitializedAsync`, so that the component automatically re-renders when the computed value is invalidated. 5. **Invalidate only the specific compute methods affected by a mutation** rather than invalidating broadly; for example, when updating a single product, invalidate `GetByIdAsync(productId)` and `GetAllAsync()` but not unrelated compute methods. 6. **Set `UpdateDelayer` in `GetStateOptions()` to control how frequently a component polls for recomputation** to balance responsiveness against server load; use `FixedDelayer.Get(TimeSpan.FromSeconds(1))` for dashboards and shorter intervals for critical data. 7. **Use Fusion's `IComputeService` interface as a marker on service interfaces** to enable the Fusion DI extensions to register the proxy wrapper automatically during `AddService()`. 8. **Do not throw exceptions from compute methods to signal "not found"** -- return `null` or empty collections instead, because exceptions bypass the computed cache and force recomputation on every access. 9. **Test compute services by verifying that calling a compute method twice returns the same cached instance** and that invalidation causes the next call to return a fresh value, ensuring the caching and invalidation graph works correctly. 10. **Separate mutation methods (Add, Update, Delete) from compute methods (Get, List, Count)** on the service interface because mutations trigger invalidation while compute methods participate in the dependency graph; mixing them creates confusing invalidation cycles.