{"id":48786694,"url":"https://github.com/rabuckley/rabstack-query","last_synced_at":"2026-04-13T17:35:22.056Z","repository":{"id":349353537,"uuid":"1121830868","full_name":"rabuckley/rabstack-query","owner":"rabuckley","description":"Powerful asynchronous state management for .NET including Blazor and MAUI","archived":false,"fork":false,"pushed_at":"2026-04-05T13:23:36.000Z","size":752,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-05T15:19:57.439Z","etag":null,"topics":["blazor","cache","dotnet","maui","state-management"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rabuckley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-23T16:16:20.000Z","updated_at":"2026-04-05T13:23:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rabuckley/rabstack-query","commit_stats":null,"previous_names":["rabuckley/rabstack-query"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/rabuckley/rabstack-query","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rabuckley%2Frabstack-query","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rabuckley%2Frabstack-query/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rabuckley%2Frabstack-query/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rabuckley%2Frabstack-query/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rabuckley","download_url":"https://codeload.github.com/rabuckley/rabstack-query/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rabuckley%2Frabstack-query/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31762775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T15:25:13.801Z","status":"ssl_error","status_checked_at":"2026-04-13T15:25:09.162Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blazor","cache","dotnet","maui","state-management"],"created_at":"2026-04-13T17:35:21.349Z","updated_at":"2026-04-13T17:35:22.046Z","avatar_url":"https://github.com/rabuckley.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RabStack Query\n\nA powerful, type-safe data synchronization library for .NET, inspired by [TanStack Query](https://tanstack.com/query) (React Query). RabStack Query provides declarative query and mutation management with automatic caching, background refetching, and optimistic updates for MAUI, Blazor, and other .NET applications.\n\n## Features\n\n- **Automatic Caching** with configurable stale time and garbage collection\n- **Stale-While-Revalidate** — serve cached data instantly while refetching in the background\n- **Background Refetching** on window focus, network reconnection, and polling intervals\n- **Optimistic Updates** with automatic rollback on error\n- **Mutations** with lifecycle hooks (onMutate, onSuccess, onError, onSettled)\n- **Infinite Queries** for cursor-based pagination\n- **Hierarchical Query Keys** with prefix-based invalidation\n- **MVVM Bindings** — `QueryViewModel`, `MutationViewModel`, `InfiniteQueryViewModel` with `INotifyPropertyChanged` and `IAsyncRelayCommand`\n- **Retry with Exponential Backoff** (1s, 2s, 4s, 8s, max 30s)\n- **Trim and AOT Safe** — no reflection, `IsTrimmable` and `IsAotCompatible`\n\n## Installation\n\n```bash\n# Core library\ndotnet add package RabstackQuery\n\n# MVVM bindings for MAUI/Blazor\ndotnet add package RabstackQuery.Mvvm\n```\n\n## Quick Start\n\nRegister the `QueryClient` in DI:\n\n```csharp\nbuilder.Services.AddRabstackQuery(options =\u003e\n{\n    options.DefaultOptions = new QueryClientDefaultOptions\n    {\n        StaleTime = TimeSpan.FromSeconds(30),\n        Retry = 2,\n    };\n});\n```\n\nUse `Scoped` (default) for Blazor, `Singleton` for MAUI:\n\n```csharp\nbuilder.Services.AddRabstackQuery(configure: _ =\u003e { }, ServiceLifetime.Singleton);\n```\n\n## Queries\n\n### Basic Query\n\n`UseQuery` creates a `QueryViewModel` that fetches data, caches it, and exposes reactive properties (`Data`, `IsLoading`, `IsError`, `Error`, `IsSuccess`, `IsStale`) via `INotifyPropertyChanged`:\n\n```csharp\npublic sealed class TodosViewModel : IDisposable\n{\n    public QueryViewModel\u003cList\u003cTodo\u003e\u003e TodosQuery { get; }\n\n    public TodosViewModel(QueryClient client, ITodoApi api)\n    {\n        TodosQuery = client.UseQuery(\n            queryKey: [\"todos\"],\n            queryFn: async ctx =\u003e await api.GetTodosAsync(ctx.CancellationToken)\n        );\n    }\n\n    public void Dispose() =\u003e TodosQuery.Dispose();\n}\n```\n\nBind directly to the ViewModel properties in XAML or Blazor:\n\n```xml\n\u003cActivityIndicator IsRunning=\"{Binding TodosQuery.IsLoading}\" /\u003e\n\u003cLabel Text=\"{Binding TodosQuery.Error.Message}\" IsVisible=\"{Binding TodosQuery.IsError}\" /\u003e\n\u003cCollectionView ItemsSource=\"{Binding TodosQuery.Data}\" IsVisible=\"{Binding TodosQuery.IsSuccess}\"\u003e\n    \u003cCollectionView.ItemTemplate\u003e\n        \u003cDataTemplate\u003e\u003cLabel Text=\"{Binding Title}\" /\u003e\u003c/DataTemplate\u003e\n    \u003c/CollectionView.ItemTemplate\u003e\n\u003c/CollectionView\u003e\n```\n\n### Query Options\n\nControl staleness, polling, placeholder data, and retries:\n\n```csharp\nTaskQuery = client.UseQuery(\n    queryKey: [\"tasks\", projectId, taskId],\n    queryFn: async ctx =\u003e await api.GetTaskAsync(projectId, taskId, ctx.CancellationToken),\n    enabled: taskId \u003e 0,\n    staleTime: TimeSpan.FromSeconds(15),\n    placeholderData: (_, _) =\u003e\n    {\n        // Seed from a parent list cache for instant perceived load\n        var cached = client.GetQueryData\u003cList\u003cTaskItem\u003e\u003e([\"tasks\", projectId]);\n        return cached?.FirstOrDefault(t =\u003e t.Id == taskId);\n    }\n);\n```\n\n### Query Keys\n\nKeys are hierarchical `List\u003cobject\u003e` with C# 12 collection expression syntax. Invalidating a prefix cascades to all queries underneath:\n\n```csharp\npublic static class QueryKeys\n{\n    public static QueryKey Projects =\u003e [\"projects\"];\n    public static QueryKey Project(int id) =\u003e [\"projects\", id];\n    public static QueryKey Tasks(int projectId) =\u003e [\"projects\", projectId, \"tasks\"];\n}\n\n// Invalidates Projects, Project(3), and Tasks(3)\nawait client.InvalidateQueriesAsync([\"projects\"]);\n```\n\n### Reusable Query Definitions\n\n`QueryOptions\u003cTData\u003e` bundles key + function + config into a single typed object (analogous to TanStack v5's `queryOptions()`). The same definition works with `UseQuery`, `FetchQueryAsync`, `GetQueryData`, and `SetQueryData`:\n\n```csharp\npublic static class Queries\n{\n    public static QueryOptions\u003cIEnumerable\u003cProject\u003e\u003e Projects(IProjectApi api) =\u003e new()\n    {\n        QueryKey = QueryKeys.Projects,\n        QueryFn = async ctx =\u003e await api.GetProjectsAsync(ctx.CancellationToken),\n        StaleTime = TimeSpan.FromSeconds(60),\n    };\n}\n\n// Observe reactively\nProjectsQuery = client.UseQuery(Queries.Projects(api));\n\n// Read cache (TData inferred)\nvar cached = client.GetQueryData(Queries.Projects(api));\n\n// Prefetch in background\nawait client.PrefetchQueryAsync(Queries.Projects(api));\n```\n\n### Select Transform\n\nCache one type, expose another. The cache stores the full object; the observer transforms it:\n\n```csharp\nTodoCountQuery = client.UseQuery(\n    queryKey: [\"todos\"],\n    queryFn: async ctx =\u003e await api.GetTodosAsync(ctx.CancellationToken),\n    select: todos =\u003e todos.Count\n);\n// TodoCountQuery.Data is int, but the cache holds List\u003cTodo\u003e\n```\n\n## Mutations\n\n`UseMutation` creates a `MutationViewModel` with `MutateCommand` (an `IAsyncRelayCommand` for XAML binding) and lifecycle callbacks:\n\n```csharp\nCreateTodoMutation = client.UseMutation\u003cTodo, string\u003e(\n    mutationFn: async (title, context, ct) =\u003e\n        await api.CreateTodoAsync(title, ct),\n    onSuccess: async (todo, title, context) =\u003e\n    {\n        await context.Client.InvalidateQueriesAsync([\"todos\"]);\n    }\n);\n\n// Fire from code\nawait CreateTodoMutation.MutateCommand.ExecuteAsync(\"Buy milk\");\n```\n\n```xml\n\u003c!-- Or bind in XAML --\u003e\n\u003cButton Text=\"Create\" Command=\"{Binding CreateTodoMutation.MutateCommand}\" CommandParameter=\"Buy milk\" /\u003e\n```\n\n### Optimistic Updates\n\nUpdate the cache before the server responds. Roll back on error:\n\n```csharp\nUpdateStatusMutation = client.UseMutation\u003cTaskItem, TaskItemStatus\u003e(\n    mutationFn: async (status, context, ct) =\u003e\n        await api.UpdateStatusAsync(taskId, status, ct),\n    options: new()\n    {\n        OnMutate = async (newStatus, context) =\u003e\n        {\n            await context.Client.CancelQueriesAsync([\"tasks\", taskId]);\n\n            var previous = context.Client.GetQueryData\u003cTaskItem\u003e([\"tasks\", taskId]);\n            if (previous is not null)\n            {\n                context.Client.SetQueryData([\"tasks\", taskId],\n                    previous with { Status = newStatus });\n            }\n\n            return null;\n        },\n        OnError = async (_, _, _, context) =\u003e\n        {\n            await context.Client.InvalidateQueriesAsync([\"tasks\", taskId]);\n        }\n    }\n);\n```\n\n## Infinite Queries\n\nCursor-based pagination with `UseInfiniteQuery`:\n\n```csharp\nCommentsQuery = client.UseInfiniteQuery(\n    new InfiniteQueryObserverOptions\u003cPagedResult\u003cComment\u003e, string?\u003e\n    {\n        QueryKey = [\"tasks\", taskId, \"comments\"],\n        QueryFn = async ctx =\u003e\n            await api.GetCommentsAsync(taskId, ctx.PageParam, pageSize: 10, ctx.CancellationToken),\n        InitialPageParam = null,\n        GetNextPageParam = ctx =\u003e ctx.Page.NextCursor is { } cursor\n            ? PageParamResult\u003cstring?\u003e.Some(cursor)\n            : PageParamResult\u003cstring?\u003e.None,\n    }\n);\n```\n\nThe `InfiniteQueryViewModel` exposes `HasNextPage`, `HasPreviousPage`, `FetchNextPageCommand`, and `FetchPreviousPageCommand` for binding.\n\n## Cache Operations\n\n```csharp\n// Read cached data\nvar todos = client.GetQueryData\u003cList\u003cTodo\u003e\u003e([\"todos\"]);\n\n// Write cached data\nclient.SetQueryData([\"todos\"], updatedTodos);\n\n// Update with a function\nclient.SetQueryData\u003cList\u003cTodo\u003e\u003e([\"todos\"], prev =\u003e prev?.Append(newTodo).ToList());\n\n// Invalidate (marks stale, triggers refetch for active observers)\nawait client.InvalidateQueriesAsync([\"todos\"]);\n\n// Prefetch (silent failures, warms cache)\nawait client.PrefetchQueryAsync(new FetchQueryOptions\u003cList\u003cTodo\u003e\u003e\n{\n    QueryKey = [\"todos\"],\n    QueryFn = async ctx =\u003e await api.GetTodosAsync(ctx.CancellationToken),\n});\n\n// Imperative fetch (throws on failure)\nvar result = await client.FetchQueryAsync(new FetchQueryOptions\u003cList\u003cTodo\u003e\u003e\n{\n    QueryKey = [\"todos\"],\n    QueryFn = async ctx =\u003e await api.GetTodosAsync(ctx.CancellationToken),\n});\n```\n\n## Focus and Network Refetching\n\nQueries automatically refetch when the app regains focus or reconnects. Wire up the platform signals:\n\n```csharp\n// MAUI\nprotected override void OnResume()\n{\n    base.OnResume();\n    FocusManager.Instance.SetFocused(true);\n}\n\nConnectivity.ConnectivityChanged += (s, e) =\u003e\n    OnlineManager.Instance.SetOnline(e.NetworkAccess == NetworkAccess.Internet);\n```\n\n## Architecture\n\nFor design decisions, reactive flow, and component responsibilities, see [`ARCHITECTURE.md`](ARCHITECTURE.md).\n\n## License\n\nMIT License - See LICENSE file for details\n\n## Acknowledgments\n\n- Inspired by [TanStack Query](https://tanstack.com/query) by Tanner Linsley\n- Built with [CommunityToolkit.Mvvm](https://github.com/CommunityToolkit/dotnet) for MVVM support\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frabuckley%2Frabstack-query","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frabuckley%2Frabstack-query","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frabuckley%2Frabstack-query/lists"}