=============== LIBRARY RULES =============== From library maintainers: - Set QueryExecutionMode.Ssr in AddFetchling for Blazor Server / SSR so IQueryClient is scoped per circuit; the default Csr registers it as a singleton. - Query TData must be a reference type; wrap value types in a record or class. - Dispose queries and view models when their owner is torn down: a live subscription pins the cache entry indefinitely. - In MVVM, use ObserveOnUi() for the Success/Failure/Settled event streams; the bindable view model properties coalesce latest-wins and would drop events. - Call MutationViewModel.ToCommand once per command, typically in a constructor, never per list row. - In tests, register a TestScheduler as the IScheduler to drive stale time, cache eviction, and refetch intervals in virtual time. ### Install Fetchling packages via dotnet CLI Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Install the required NuGet packages. The Fetchling package includes core and DI integration; others are optional depending on your use case. ```bash dotnet add package Fetchling ``` ```bash dotnet add package Fetchling.Core ``` ```bash dotnet add package Fetchling.Extensions.DependencyInjection ``` ```bash dotnet add package Fetchling.Blazor ``` ```bash dotnet add package Fetchling.Blazor.DevTools ``` ```bash dotnet add package Fetchling.Mvvm ``` -------------------------------- ### Use shortcut streams for success and failure Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Subscribes to the Success and Failure streams to handle only the outcome you care about. ```csharp userQueries.UserQuery.Success.Subscribe(user => Console.WriteLine($"Got user: {user.Name}")); userQueries.UserQuery.Failure.Subscribe(error => Console.WriteLine($"Failed: {error.Message}")); ``` -------------------------------- ### Trigger a fetch with SetArgs Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Pushes arguments to the query to start fetching. In this example, it fetches user 42. ```csharp // Trigger a fetch for user 42 userQueries.UserQuery.SetArgs(42); ``` -------------------------------- ### Register Fetchling with DI using AddFetchling Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Minimal setup to register Fetchling services in your DI container. Call AddFetchling in Program.cs or wherever services are configured. ```csharp builder.Services.AddFetchling(); ``` -------------------------------- ### Create IQueryClient without DI using QueryClientFactory Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Use QueryClientFactory to create a client manually when not using a DI container. Remember to dispose the client when done. ```csharp IQueryClient client = QueryClientFactory.Create(new QueryClientOptions { StaleTime = TimeSpan.FromMinutes(1), }); ``` -------------------------------- ### Install Fetchling packages via dotnet add package Source: https://github.com/psachmann/fetchling/blob/main/README.md Install the Fetchling NuGet packages. The first command adds the core and DI integration in one package, which is the usual starting point. The remaining commands add the core library, DI integration, Blazor components, Blazor DevTools for a live cache inspector, and MVVM view models for MAUI, WPF, WinUI, UNO, and Avalonia. ```bash dotnet add package Fetchling ``` ```bash dotnet add package Fetchling.Core ``` ```bash dotnet add package Fetchling.Extensions.DependencyInjection ``` ```bash dotnet add package Fetchling.Blazor ``` ```bash dotnet add package Fetchling.Blazor.DevTools ``` ```bash dotnet add package Fetchling.Mvvm ``` -------------------------------- ### Register service classes in Program.cs Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Adds the UserQueries and UserMutations services to the DI container for Blazor. Required before injecting them into components. ```csharp // Program.cs builder.Services.AddScoped(); builder.Services.AddScoped(); ``` -------------------------------- ### Define a query with key factory and fetcher Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Creates a query that fetches a user by ID. The fetcher throws if the user is not found. The query does not fetch until args are pushed. ```csharp public sealed class UserQueries(IQueryClient queryClient, HttpClient httpClient) : IDisposable { public readonly IQuery UserQuery = queryClient.CreateQuery( new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = async (id, ct) => await httpClient.GetFromJsonAsync($"/api/users/{id}", ct) ?? throw new InvalidOperationException("User not found."), } ); public void Dispose() => UserQuery.Dispose(); } ``` -------------------------------- ### Execute the mutation Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Triggers the mutation by calling Execute with a CreateUserRequest. This is the direct invocation of the mutation defined in the previous example. ```csharp userMutations.CreateUser.Execute(new CreateUserRequest { Name = "Alice" }); ``` -------------------------------- ### Prefetch multiple keys in parallel Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/prefetching.md Warms the first page of results by prefetching multiple keys concurrently. Each key gets its own cache entry without cross-contamination. ```csharp // Warm the first page of results in parallel await Task.WhenAll( facade.UserQuery.PrefetchAsync(1), facade.UserQuery.PrefetchAsync(2), facade.UserQuery.PrefetchAsync(3) ); ``` -------------------------------- ### MutationOptions Reference Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Full MutationOptions example showing all available options: Mutator, RetryHandler, Name, IsEnabled, InvalidateKeys, OnMutate, OnSuccess, OnFailure, and OnSettled. Only Mutator is required. ```csharp new MutationOptions { // Required: the async operation Mutator = (request, ct) => httpClient.PostAsJsonAsync("/api/users", request, ct), // Override the global retry handler for this mutation RetryHandler = new NoRetryHandler(), // Name used to tag metrics, traces, and log messages — defaults to typeof(TArgs).Name Name = "createUser", // Start disabled — Execute() calls are silently ignored until enabled IsEnabled = false, // Invalidate these query keys automatically on success InvalidateKeys = [QueryKey.From("users")], // Called synchronously before the mutator runs — use for optimistic updates OnMutate = request => { /* snapshot + SetData */ }, // Called after a successful execution, before OnSettled OnSuccess = (request, user) => logger.LogInformation("Created user {Id}", user.Id), // Called after a failed execution (after all retries), before OnSettled OnFailure = error => logger.LogError(error, "Failed to create user"), // Called after every terminal state: success, failure, or cancellation OnSettled = () => isBusy = false, } ``` -------------------------------- ### Install Fetchling.Blazor package Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/blazor.md Adds the Fetchling.Blazor NuGet package to the project. ```bash dotnet add package Fetchling.Blazor ``` -------------------------------- ### Install Fetchling.Blazor.DevTools package Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/blazor.md Adds the QueryDevTools package to the project via the dotnet CLI. ```bash dotnet add package Fetchling.Blazor.DevTools ``` -------------------------------- ### Subscribe to the state stream Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Reacts to query state changes: shows a spinner while fetching, renders data on success, and shows an error on failure. ```csharp userQueries.UserQuery.State.Subscribe(state => { if (state.IsFetching) ShowSpinner(); if (state.IsSuccess) Render(state.CurrentData!); if (state.IsFailure) ShowError(state.Error!.Message); }); ``` -------------------------------- ### Fetcher example Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Fetches a page of data. Receives args, the current page param, and a CancellationToken. ```csharp Fetcher = (listId, page, ct) => api.GetPostsAsync(listId, page, PageSize, ct) ``` -------------------------------- ### QueryKey construction examples Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/introduction.md Demonstrates how to create query keys with one or more parts to identify cache entries, e.g., for all users, a specific user, or a user's posts. ```csharp QueryKey.From("users") // all users QueryKey.From("users", 42) // user with id 42 QueryKey.From("users", 42, "posts") // posts for user 42 ``` -------------------------------- ### InitialData example Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Pre-seeds the first page from a local cache or SSR payload. The query starts in Success state and triggers a background refetch. Only the first page is seeded; subsequent pages require FetchNextPage. ```csharp public readonly IInfiniteQuery, int> PostsQuery = queryClient.CreateInfiniteQuery(new InfiniteQueryOptions, int> { KeyFactory = boardId => ["posts", boardId], Fetcher = (boardId, page, ct) => api.GetPostsAsync(boardId, page, PageSize, ct), InitialPageParam = 1, GetNextPageParam = info => info.Page.Count < PageSize ? PageParam.None : PageParam.Some(info.PageParam + 1), InitialData = cachedFirstPage, // IReadOnlyList from local storage / SSR }); ``` -------------------------------- ### Read current state synchronously Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Gets the current state without subscribing, useful for rendering without a subscription. ```csharp var state = userQueries.UserQuery.CurrentState; ``` -------------------------------- ### Configure global defaults with AddFetchling options Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Configure global defaults for stale time, cache time, and refetch interval. These settings control data freshness and caching behavior. ```csharp builder.Services.AddFetchling(options => { options.StaleTime = TimeSpan.FromMinutes(1); // data stays fresh for 1 minute options.CacheTime = TimeSpan.FromMinutes(10); // cache entries live 10 minutes after last subscriber options.RefetchInterval = TimeSpan.FromSeconds(30); // automatically refetch every 30 seconds }); ``` -------------------------------- ### InitialPageParam examples Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Sets the page param for the first fetch. Use 1 for offset/page-number pagination, or Guid.Empty for cursor-based pagination. ```csharp InitialPageParam = 1 // offset / page-number pagination ``` ```csharp InitialPageParam = Guid.Empty // cursor-based, "before all items" ``` -------------------------------- ### QueryOptions Configuration Example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to configure QueryOptions with required KeyFactory and Fetcher, plus optional overrides like StaleTime, CacheTime, RefetchInterval, RetryHandler, IsEnabled, InitialData, and DataComparer. InitialData is always treated as stale. ```csharp new QueryOptions { // Required KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => httpClient.GetFromJsonAsync($"/api/users/{id}", ct)!, // Optional overrides (null = use global default) StaleTime = TimeSpan.FromMinutes(5), CacheTime = TimeSpan.FromMinutes(30), RefetchInterval = TimeSpan.FromSeconds(30), // poll while subscribers exist RetryHandler = new MyRetryHandler(), IsEnabled = true, // default; set false for conditional/lazy queries InitialData = null, // pre-populate cache before first fetch; always treated as stale // Comparer to suppress re-emissions when fetched data is structurally identical DataComparer = EqualityComparer.Default, } ``` -------------------------------- ### Enable logging with DI Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/observability.md Registers Fetchling with dependency injection; the ILoggerFactory is resolved automatically, so no extra configuration is needed. ```csharp // DI (recommended) — no extra configuration needed builder.Services.AddFetchling(); ``` -------------------------------- ### Create UserFacade with PrefetchAsync Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/prefetching.md Defines a facade class that owns a query and exposes it for prefetching. The query uses a KeyFactory and Fetcher with a 5-minute stale time. Inject this facade into components instead of IQueryClient. ```csharp public class UserFacade { private readonly IUserApi _api; public IQuery UserQuery { get; } public UserFacade(IQueryClient client, IUserApi api) { _api = api; UserQuery = client.CreateQuery(new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => _api.GetUserAsync(id, ct), StaleTime = TimeSpan.FromMinutes(5), }); } } ``` -------------------------------- ### SyncFrom usage examples Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mvvm.md Two usage examples: one for a sidebar list of plain records using the same-type overload, and one for row view models using the projection overload. The projection overload refreshes existing rows via update instead of replacing them, preserving bound selections. ```csharp // Sidebar list: TodoList is a plain record, so the same-type overload is enough. Lists.SyncFrom(freshLists, list => list.Id); // Row view models: the projection overload builds TodoItemViewModel from TodoItem, // and refreshes an existing row in place via `update` rather than replacing it. ItemRows.SyncFrom( freshItems, model => model.Id, row => row.Model.Id, model => new TodoItemViewModel(model), (row, model) => row.Update(model)); ``` -------------------------------- ### InfiniteSuspense component example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt For IInfiniteQuery. Shows Loading during initial load and full background refetches. Content receives full InfiniteQueryState to render all pages and wire up Load more. ```razor @foreach (var item in state.Pages.SelectMany(p => p)) {
@item.Name
} @if (state.HasNextPage) { }

Loading…

Error: @e.Message

``` -------------------------------- ### Subscribe to Fetchling source Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/observability.md Configures OpenTelemetry tracing and metrics in Program.cs, subscribing to the "Fetchling" source and using the console exporter. Any OpenTelemetry-compatible exporter works. ```csharp builder.Services.AddOpenTelemetry() .WithTracing(tracing => tracing .AddSource(QueryTelemetry.SourceName) .AddConsoleExporter()) .WithMetrics(metrics => metrics .AddMeter(QueryTelemetry.SourceName) .AddConsoleExporter()); ``` -------------------------------- ### KeyFactory example Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Defines the cache key for an infinite query. All observers with the same key share one InfiniteQuery instance. ```csharp KeyFactory = listId => ["posts", listId] ``` -------------------------------- ### Subscribe to mutation state Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Subscribes to the mutation's state to react to changes. The callback checks IsRunning, IsSuccess, and IsFailure to show a spinner, navigate, or display an error message. ```csharp userMutations.CreateUser.State.Subscribe(state => { if (state.IsRunning) ShowSpinner(); if (state.IsSuccess) NavigateTo("/users"); if (state.IsFailure) ShowError(state.Error!.Message); }); ``` -------------------------------- ### Suspense component example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows Loading while fetching (including background refetches), Content on success, Failure on error. Query and Content are required; Loading and Failure are optional. ```razor

@user.Name

Loading...

Error: @error.Message

``` -------------------------------- ### UserPageViewModel usage example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Typical page view model that wraps a QueryViewModel for a user fetch. Uses IQueryClient to create the query, with a KeyFactory, Fetcher, and StaleTime. The view model owns and disposes the query. ```csharp public sealed class UserPageViewModel : IDisposable { public UserPageViewModel(IQueryClient queryClient, IUserApi api) => User = new QueryViewModel(queryClient, new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => api.GetUserAsync(id, ct), StaleTime = TimeSpan.FromMinutes(5), }); public QueryViewModel User { get; } public void Load(int userId) => User.SetArgs(userId); public void Dispose() => User.Dispose(); } ``` -------------------------------- ### Install Fetchling.Mvvm package Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mvvm.md Adds the Fetchling.Mvvm NuGet package, which depends only on Fetchling.Core and includes no MVVM toolkit or platform frameworks. ```bash dotnet add package Fetchling.Mvvm ``` -------------------------------- ### Add OpenTelemetry packages Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/observability.md Adds the OpenTelemetry hosting and console exporter packages to the app project, not the library. The console exporter is one option; any exporter works. ```bash dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Exporter.Console # or any other exporter ``` -------------------------------- ### Create a Mutation with IQueryClient.CreateMutation Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Use IQueryClient.CreateMutation to create a mutation with MutationOptions. Only Mutator is required; all other options are optional. ```csharp IMutation mutation = queryClient.CreateMutation(new MutationOptions { Mutator = (args, ct) => ..., }); ``` -------------------------------- ### Enable logging without DI Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/observability.md Creates a logger factory and logger manually, then passes the logger to the client factory. Use QueryTelemetry.SourceName as the logger category. ```csharp ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole()); ILogger logger = loggerFactory.CreateLogger(QueryTelemetry.SourceName); IQueryClient client = QueryClientFactory.Create(new QueryClientOptions(), logger: logger); ``` -------------------------------- ### QueryTelemetry entry point Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/observability.md Defines the public telemetry entry point with an ActivitySource and Meter named "Fetchling". Use QueryTelemetry.SourceName when subscribing to traces or metrics. ```csharp // Fetchling.Core.Observability public static class QueryTelemetry { public const string SourceName = "Fetchling"; public static readonly ActivitySource ActivitySource = new(SourceName); public static readonly Meter Meter = new(SourceName); } ``` -------------------------------- ### PageParam usage examples Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to signal page boundaries with PageParam.None, PageParam.Some(value), and implicit conversion from the underlying type. ```csharp // Signal "no more pages" PageParam.None // Signal "next page param is 2" PageParam.Some(2) // Implicit conversion from the underlying type PageParam p = 2; // equivalent to Some(2) ``` -------------------------------- ### InfiniteTransition component example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt For IInfiniteQuery. Stale-while-revalidate: shows Content whenever HasData, including during full background refetches and FetchNextPage. Shows Loading only before first page arrives. ```razor @foreach (var item in state.Pages.SelectMany(p => p)) {
@item.Name
} @if (state.HasNextPage) { }

Loading…

Error: @e.Message

``` -------------------------------- ### Query deduplication with same options Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/caching.md Shows two components creating queries with identical options; only one fetch runs for a given key, and both receive the same QueryState transitions. ```csharp // Component A var queryA = queryClient.CreateQuery(options); // same options as Component B queryA.SetArgs(42); // triggers a fetch // Component B (maybe rendered at the same time) var queryB = queryClient.CreateQuery(options); queryB.SetArgs(42); // cache hit — no second fetch ``` -------------------------------- ### PageParam usage examples Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Shows the three ways to create a PageParam: None, Some(2), and implicit conversion from int. The implicit conversion is equivalent to Some(2). ```csharp PageParam.None // no more pages PageParam.Some(2) // next page param is 2 (PageParam)2 // implicit conversion — equivalent to Some(2) ``` -------------------------------- ### Prefetch a single key Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/prefetching.md Calls PrefetchAsync on the query to warm the cache before the detail component mounts. Skips the fetch if fresh data exists within stale time. ```csharp // Warm the cache before the detail component mounts await facade.UserQuery.PrefetchAsync(userId); ``` -------------------------------- ### KeyFactory examples: simple, parameterized, compound keys Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/queries.md Shows three KeyFactory patterns: constant key, parameterized key per id, and compound key per user and type. Keys are hierarchical for invalidation. ```csharp // Simple key — always the same data regardless of args KeyFactory = _ => QueryKey.From("config") // Parameterized key — different data per id KeyFactory = id => QueryKey.From("users", id) // Compound key — different data per user and type KeyFactory = (userId, type) => QueryKey.From("users", userId, "items", type) ``` -------------------------------- ### Subscribe to shortcut streams for success, failure, and settled Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Subscribe to shortcut streams for success, failure, and settled events. These provide direct access to the data or error without checking status. ```csharp mutation.Success.Subscribe(data => NavigateTo($"/users/{data.Id}")); mutation.Failure.Subscribe(error => ShowError(error.Message)); mutation.Settled.Subscribe(_ => HideSpinner()); ``` -------------------------------- ### Per-row and page-VM command creation examples Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to build commands from a MutationViewModel. Per-row commands map the command parameter to TArgs; page-VM commands pull args from the view model's own state. ToCommand must be called once per command to avoid leaks. ```csharp // Per-row command: parameter maps to TArgs, one shared instance handed to every row // (calling ToCommand per row would leak -- see above). public ICommand ToggleCommand { get; } = ToggleItem.ToCommand(item => new ToggleArgs(item.Id)); // Args come from the page VM's own state instead of the command parameter. public ICommand AddItemCommand { get; } = AddItem.ToCommand(() => new AddItemArgs(NewItemText)); ``` -------------------------------- ### Sample progress stream to throttle UI updates Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Sample the progress stream to throttle UI updates. Reports are not throttled, so a mutator reporting on every chunk can emit many states. Sampling reduces UI updates. ```csharp _subscription = upload.State .Sample(TimeSpan.FromMilliseconds(100)) .Subscribe(state => InvokeAsync(() => { _state = state; StateHasChanged(); })); ``` -------------------------------- ### Quick Example: Registering Fetchling and Creating a Query Source: https://github.com/psachmann/fetchling/blob/main/docs/index.md Shows how to register Fetchling in Program.cs with a stale time option and create a query with a key factory and fetcher. Subscribe to Success and Failure observables to handle results. ```csharp // Register in Program.cs builder.Services.AddFetchling(options => { options.StaleTime = TimeSpan.FromMinutes(1); }); // In your service or component var query = queryClient.CreateQuery(new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => userService.GetByIdAsync(id, ct), }); query.SetArgs(42); query.Success.Subscribe(user => Console.WriteLine($"Hello, {user.Name}!")); query.Failure.Subscribe(error => Console.WriteLine($"Oops: {error.Message}")); ``` -------------------------------- ### InitialData Option Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Pre-populates the first page of an infinite query from a local cache, SSR payload, or any synchronous source. The query starts in Success state with no loading flash and immediately triggers a background refetch to stay fresh. ```APIDOC ## InitialData Option ### Description Pre-populates the first page of an infinite query from a local cache, SSR payload, or any synchronous source. The query starts in Success state with no loading flash and immediately triggers a background refetch to stay fresh. ### Method Option ### Endpoint InfiniteQueryOptions.InitialData ### Parameters #### Request Body - **InitialData** (TData?) - Optional - Pre-seeds the first page; query starts in Success state and immediately displays seeded data while a background refetch refreshes it. ### Response #### Success Response (200) - **IInfiniteQuery** - The created infinite query with the seeded first page. ### Request Example ```csharp public readonly IInfiniteQuery, int> PostsQuery = queryClient.CreateInfiniteQuery(new InfiniteQueryOptions, int> { KeyFactory = boardId => ["posts", boardId], Fetcher = (boardId, page, ct) => api.GetPostsAsync(boardId, page, PageSize, ct), InitialPageParam = 1, GetNextPageParam = info => info.Page.Count < PageSize ? PageParam.None : PageParam.Some(info.PageParam + 1), InitialData = cachedFirstPage, // IReadOnlyList from local storage / SSR }); ``` ``` -------------------------------- ### Execute Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Executes the mutation with the provided arguments. If a previous execution is still running, it is cancelled and the new one starts immediately. If the mutation is disabled, the call is silently ignored. ```APIDOC ## Execute ### Description Executes the mutation with the provided arguments. If a previous execution is still running, it is cancelled and the new one starts immediately. If the mutation is disabled, the call is silently ignored. ### Method Execute ### Parameters #### Request Body - **args** (CreateUserRequest) - Required - The arguments to pass to the mutation. ### Request Example ```csharp mutation.Execute(new CreateUserRequest { Name = "Alice" }); ``` ``` -------------------------------- ### Render query state with Suspense in Blazor Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Uses the Suspense component to declaratively handle query loading, success, and failure states. The query arguments are set in OnInitialized. ```html @inject UserQueries Queries

Hello, @user.Name!

Loading...

Something went wrong: @error.Message

@code { protected override void OnInitialized() { Queries.UserQuery.SetArgs(42); } } ``` -------------------------------- ### Subscribing to query state and event streams Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/introduction.md Shows how to observe query state transitions, success data, failures, and settled events via Rx.NET observables. ```csharp query.State.Subscribe(state => { /* every transition */ }); query.Success.Subscribe(data => { /* unwrapped data on success */ }); query.Failure.Subscribe(error => { /* exception on failure */ }); query.Settled.Subscribe(state => { /* after any fetch completes */ }); ``` -------------------------------- ### Invalidate by Predicate Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/caching.md Invalidates all entries matching a predicate. Useful after mutations that affect multiple queries. The first example invalidates all user-related entries; the second invalidates everything. ```csharp // Invalidate all user-related entries queryClient.Invalidate(key => key.ToString().StartsWith("users")); ``` ```csharp // Invalidate everything queryClient.Invalidate(_ => true); ``` -------------------------------- ### MutationOptions configuration example Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows required Mutator and optional settings like RetryHandler, IsEnabled, InvalidateKeys, and lifecycle callbacks. Execution order is OnMutate → mutator → InvalidateKeys → OnSuccess/OnFailure → OnSettled; OnSettled fires on cancellation too. InvalidateKeys fires when the mutator's Task resolves; for eventually-consistent backends, use OnSuccess with manual Invalidate. ```csharp new MutationOptions { // Required Mutator = (request, ct) => httpClient.PostAsJsonAsync("/api/users", request, ct), // Optional RetryHandler = null, // null = use global default IsEnabled = true, InvalidateKeys = [QueryKey.From("users")], // invalidated automatically on success // Lifecycle callbacks — execution order: OnMutate → mutator → InvalidateKeys → OnSuccess/OnFailure → OnSettled OnMutate = args => { /* snapshot + apply optimistic update */ }, OnSuccess = (args, result) => toast.Show($"Created {result.Name}"), OnFailure = error => toast.Show($"Error: {error.Message}"), OnSettled = () => isBusy = false, // fires for success, failure, AND cancellation } ``` -------------------------------- ### Mutator Example with CancellationToken Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md The mutator is an async function that performs the operation. It receives args and a CancellationToken that is cancelled when the mutation is cancelled or disposed. Always pass the CancellationToken to async operations. ```csharp Mutator = (request, ct) => httpClient.PostAsJsonAsync("/api/users", request, ct) ``` -------------------------------- ### Subscribe to mutation state for form handling in Blazor Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Subscribes to the CreateUser mutation's state to navigate on success and refresh the UI. The component disposes only its own subscription; the mutation is owned by the injected service. ```html @inject UserMutations Mutations @inject NavigationManager Nav @implements IDisposable @code { private IDisposable? _subscription; protected override void OnInitialized() { _subscription = Mutations.CreateUser.State.Subscribe(state => { if (state.IsSuccess) Nav.NavigateTo($"/users/{state.CurrentData!.Id}"); InvokeAsync(StateHasChanged); }); } private void HandleCreate() => Mutations.CreateUser.Execute(new CreateUserRequest { Name = "Alice" }); public void Dispose() => _subscription?.Dispose(); } ``` -------------------------------- ### Creating a client without DI Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to instantiate IQueryClient directly without dependency injection. The scheduler and logger parameters are optional; they default to DefaultScheduler.Instance and NullLogger respectively. ```csharp IQueryClient client = QueryClientFactory.Create( new QueryClientOptions { StaleTime = TimeSpan.FromMinutes(1) }, scheduler: null, // defaults to DefaultScheduler.Instance logger: null // defaults to NullLogger ); ``` -------------------------------- ### Execute a mutation with CreateUserRequest Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Call Execute with the args to trigger the mutation. If a previous execution is still running, it is cancelled and the new one starts immediately. If the mutation is disabled (IsEnabled = false or SetEnabled(false)), Execute is silently ignored. ```csharp mutation.Execute(new CreateUserRequest { Name = "Alice" }); ``` -------------------------------- ### Define a mutation with a mutator function Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/getting-started.md Creates a mutation for creating a user. The mutator posts JSON to /api/users and returns the deserialized UserDto. Automatically invalidates the 'users' list query on success and provides OnSuccess, OnFailure, and OnSettled callbacks. The class implements IDisposable to dispose the mutation. ```csharp public sealed class UserMutations(IQueryClient queryClient, HttpClient httpClient) : IDisposable { public readonly IMutation CreateUser = queryClient.CreateMutation(new MutationOptions { Mutator = async (request, ct) => { var response = await httpClient.PostAsJsonAsync("/api/users", request, ct); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("The server returned no user."); }, // Automatically invalidate the "users" list query after a successful creation InvalidateKeys = [QueryKey.From("users")], OnSuccess = (request, user) => Console.WriteLine($"Created user {user.Id}"), OnFailure = error => Console.WriteLine($"Failed: {error.Message}"), OnSettled = () => Console.WriteLine("Mutation finished"), }); public void Dispose() => CreateUser.Dispose(); } ``` -------------------------------- ### Enable IncludeQueryKeyInMetrics Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/migrating-to-v2.md Enable IncludeQueryKeyInMetrics ```csharp builder.Services.AddDotNetQuery(options => { options.IncludeQueryKeyInMetrics = true; }); ``` -------------------------------- ### Conditional Queries: start disabled and enable later Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Creates a query that starts disabled with IsEnabled = false. Later, when the user selects an ID, enable it and set arguments. ```csharp var query = queryClient.CreateQuery(new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => api.GetUserAsync(id!.Value, ct), IsEnabled = false, // start disabled }); // Later, when user selects an ID: query.SetEnabled(true); query.SetArgs(selectedUserId); ``` -------------------------------- ### SetEnabled: Start disabled and enable with args Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/queries.md Starts the query disabled and enables it later when a valid ID is selected. The query is created with IsEnabled = false, then SetEnabled(true) and SetArgs are called. ```csharp // Start disabled; enable when we have a valid ID var query = queryClient.CreateQuery(new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => ..., IsEnabled = false, }); // Later, when the user selects an ID: query.SetEnabled(true); query.SetArgs(selectedUserId); ``` -------------------------------- ### Create a query with IQueryClient.CreateQuery Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/queries.md Creates a query with required KeyFactory and Fetcher properties. Other options override global defaults from QueryClientOptions. ```csharp IQuery query = queryClient.CreateQuery(new QueryOptions { KeyFactory = args => QueryKey.From(...), Fetcher = (args, ct) => ..., }); ``` -------------------------------- ### CreateQuery Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Gets or creates a query. Queries with the same key share one cache entry. ```APIDOC ## CreateQuery ### Description Gets or creates a query. Queries with the same key share one cache entry. ### Method CreateQuery ### Signature IQuery CreateQuery(QueryOptions options); ### Parameters - **options** (QueryOptions) - Required - The query options. ### Returns - **IQuery** - The query instance. ``` -------------------------------- ### QueryClientFactory.Create Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Creates a new IQueryClient instance without dependency injection. ```APIDOC ## QueryClientFactory.Create ### Description Creates a new IQueryClient instance without dependency injection. ### Method Create ### Signature IQueryClient client = QueryClientFactory.Create( new QueryClientOptions { StaleTime = TimeSpan.FromMinutes(1) }, scheduler: null, // defaults to DefaultScheduler.Instance logger: null // defaults to NullLogger ); ### Parameters - **options** (QueryClientOptions) - Required - The client options. - **scheduler** (IScheduler?) - Optional - The scheduler to use. Defaults to DefaultScheduler.Instance. - **logger** (ILogger?) - Optional - The logger to use. Defaults to NullLogger. ### Returns - **IQueryClient** - The created client instance. ``` -------------------------------- ### Dispose mutation Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Mutations implement IDisposable, so call Dispose when the mutation is no longer needed. ```csharp mutation.Dispose(); ``` -------------------------------- ### MutationOptions Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Configuration options for creating a mutation. Only the Mutator is required; all other properties are optional. ```APIDOC ## MutationOptions ### Description Configuration options for creating a mutation. Only the Mutator is required; all other properties are optional. ### Properties - **Mutator** (Func>) - Required - The async function that performs the operation. - **RetryHandler** (IRetryHandler) - Optional - Overrides the global retry handler for this mutation. - **Name** (string) - Optional - Used to tag metrics, traces, and log messages; defaults to typeof(TArgs).Name. - **IsEnabled** (bool) - Optional - If false, Execute() calls are silently ignored until enabled. - **InvalidateKeys** (IEnumerable) - Optional - Query keys to invalidate automatically on success. - **OnMutate** (Action) - Optional - Called synchronously before the mutator runs; use for optimistic updates. - **OnSuccess** (Action) - Optional - Called after a successful execution, before OnSettled. - **OnFailure** (Action) - Optional - Called after a failed execution (after all retries), before OnSettled. - **OnSettled** (Action) - Optional - Called after every terminal state: success, failure, or cancellation. ### Example ```csharp new MutationOptions { Mutator = (request, ct) => httpClient.PostAsJsonAsync("/api/users", request, ct), RetryHandler = new NoRetryHandler(), Name = "createUser", IsEnabled = false, InvalidateKeys = [QueryKey.From("users")], OnMutate = request => { /* snapshot + SetData */ }, OnSuccess = (request, user) => logger.LogInformation("Created user {Id}", user.Id), OnFailure = error => logger.LogError(error, "Failed to create user"), OnSettled = () => isBusy = false, } ``` ``` -------------------------------- ### ItemService with IInfiniteQuery (C#) Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Defines a service that creates an infinite query for paged item lists. Uses IQueryClient and IApi, with a page size of 20, a key factory based on list ID, and a fetcher that calls GetItemsAsync. The query determines the next page parameter based on whether the current page is full. ```csharp public sealed class ItemService(IQueryClient client, IApi api) : IDisposable { private const int PageSize = 20; public readonly IInfiniteQuery, int> ItemsQuery = client.CreateInfiniteQuery(new InfiniteQueryOptions, int> { KeyFactory = listId => ["items", listId], Fetcher = (listId, page, ct) => api.GetItemsAsync(listId, page, PageSize, ct), InitialPageParam = 1, GetNextPageParam = info => info.Page.Count < PageSize ? PageParam.None : PageParam.Some(info.PageParam + 1), }); public void Dispose() => ItemsQuery.Dispose(); } ``` -------------------------------- ### Upgrade all DotNetQuery packages together Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/migrating-to-v2.md Run these dotnet add package commands to upgrade all DotNetQuery packages to matching versions. The packages depend on each other at matching versions, so upgrade them all together. After upgrading, rebuild; source-breaking changes produce compiler errors, while behavior and telemetry changes compile silently. ```bash dotnet add package DotNetQuery.Core dotnet add package DotNetQuery.Extensions.DependencyInjection dotnet add package DotNetQuery.Blazor dotnet add package DotNetQuery.Blazor.DevTools dotnet add package DotNetQuery.Mvvm ``` -------------------------------- ### CreateInfiniteQuery Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Gets or creates an infinite (paginated) query. Queries with the same key share one cache entry. ```APIDOC ## CreateInfiniteQuery ### Description Gets or creates an infinite (paginated) query. Queries with the same key share one cache entry. ### Method CreateInfiniteQuery ### Signature IInfiniteQuery CreateInfiniteQuery( InfiniteQueryOptions options); ### Parameters - **options** (InfiniteQueryOptions) - Required - The infinite query options. ### Returns - **IInfiniteQuery** - The infinite query instance. ``` -------------------------------- ### InfiniteQueryOptions configuration Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Configures an infinite query with required KeyFactory, Fetcher, InitialPageParam, and GetNextPageParam, plus optional settings like MaxPages, StaleTime, and InitialData. ```csharp new InfiniteQueryOptions, int> { // Required KeyFactory = id => ["items", id], Fetcher = (id, page, ct) => api.GetItemsAsync(id, page, ct), InitialPageParam = 1, GetNextPageParam = info => info.Page.Count < PageSize ? PageParam.None : PageParam.Some(info.PageParam + 1), // Optional GetPreviousPageParam = null, // omit for forward-only lists MaxPages = null, // unbounded by default; set to trim oldest pages StaleTime = null, CacheTime = null, RefetchInterval = null, RetryHandler = null, IsEnabled = true, DataComparer = null, InitialData = null, // pre-seed first page; query starts Success, refetches in background } ``` -------------------------------- ### Invalidate by Key Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/caching.md Marks a cache entry as stale by its exact key. Only matches the exact key `users:42`. ```csharp queryClient.Invalidate(QueryKey.From("users", 42)); ``` -------------------------------- ### QueryKey creation and Default sentinel Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to create immutable cache keys with QueryKey.From and access the QueryKey.Default sentinel returned by IQuery.Key before SetArgs is called. QueryKey.ToString() joins parts with ':' for predicate-based invalidation. ```csharp // Create keys QueryKey.From("users") // simple QueryKey.From("users", 42) // parameterized — different entry per id QueryKey.From("users", 42, "posts") // compound/hierarchical // Sentinel for the uninitialized state QueryKey.Default // returned by IQuery.Key before SetArgs is called ``` -------------------------------- ### Client-level invalidation Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Invalidates infinite queries at the client level, same as regular queries. Two examples: by key array and by predicate. ```csharp queryClient.Invalidate(["posts", listId]); queryClient.Invalidate(key => key.ToString().StartsWith("posts")); ``` -------------------------------- ### Create a query with Fetchling Source: https://github.com/psachmann/fetchling/blob/main/README.md Creates a query for fetching a user by ID. The query uses a key factory and fetcher, sets arguments, and subscribes to success and failure events. ```csharp // Create a query var query = queryClient.CreateQuery(new QueryOptions { KeyFactory = id => QueryKey.From("users", id), Fetcher = (id, ct) => userService.GetByIdAsync(id, ct), }); query.SetArgs(42); query.Success.Subscribe(user => Console.WriteLine($"Hello, {user.Name}!")); query.Failure.Subscribe(error => Console.WriteLine($"Oops: {error.Message}")); ``` -------------------------------- ### Optimistic Updates: mutation with snapshot and rollback Source: https://github.com/psachmann/fetchling/blob/main/docs/llms.txt Shows how to implement optimistic updates: snapshot current data, cancel in-flight fetch, apply optimistic value, and roll back on failure. On success, invalidates keys to replace optimistic data with server-confirmed value. ```csharp UserDto? _snapshot = null; UpdateUser = queryClient.CreateMutation(new MutationOptions { Mutator = (user, ct) => api.UpdateUserAsync(user, ct), OnMutate = user => { _snapshot = UserQuery.CurrentState.CurrentData; // 1. snapshot UserQuery.Cancel(); // 2. stop in-flight fetch UserQuery.SetData(user); // 3. apply optimistic value }, OnFailure = _ => { if (_snapshot is not null) UserQuery.SetData(_snapshot); // 4. roll back on error }, // On success: replace optimistic data with the server-confirmed value InvalidateKeys = [QueryKey.From("users", ...)], }); ``` -------------------------------- ### Disable and re-enable a mutation Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/mutations.md Disable a mutation to prevent execution; Execute() calls are silently ignored. Re-enable with SetEnabled(true). ```csharp mutation.SetEnabled(false); // Execute() calls are silently ignored mutation.SetEnabled(true); // re-enables ``` -------------------------------- ### Automatic Refetch Interval Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/caching.md Sets `RefetchInterval` to poll queries in the background while they have active subscribers. The interval stops when the last subscriber unsubscribes. ```csharp new QueryOptions { KeyFactory = _ => QueryKey.From("dashboard"), Fetcher = (_, ct) => dashboardService.GetAsync(ct), RefetchInterval = TimeSpan.FromSeconds(30), // refresh every 30 seconds } ``` -------------------------------- ### Prefetch with cancellation token Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/prefetching.md Passes an optional CancellationToken to PrefetchAsync. If cancelled during the fetch, the cache entry is left in Idle state and no exception is thrown. ```csharp await facade.UserQuery.PrefetchAsync(userId, cancellationToken); ``` -------------------------------- ### Prefetch on hover Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/prefetching.md Triggers prefetching during navigation, e.g., when the user hovers over an item in a list. Ensures data is cached before the detail component mounts. ```csharp private async Task OnItemHover(int userId) { await _facade.UserQuery.PrefetchAsync(userId); } ``` -------------------------------- ### SetArgs for initial load Source: https://github.com/psachmann/fetchling/blob/main/docs/doc/guides/infinite-queries.md Provides the args for the query. The first fetch uses InitialPageParam. ```csharp query.SetArgs(listId); ```