Getting Started
This guide walks you through installing DotNet Query and writing your first query and mutation. By the end you will have a working setup and a solid foundation to build on.
Prerequisites
- .NET 10.0 or later
- A project that uses
Microsoft.Extensions.DependencyInjection(ASP.NET Core, Blazor, or any generic host) — or just the factory if you are wiring things up manually.
Installation
Install the packages you need from NuGet:
# Core library — always required
dotnet add package DotNetQuery.Core
# DI integration — if you use Microsoft.Extensions.DependencyInjection
dotnet add package DotNetQuery.Extensions.DependencyInjection
# Blazor components — if you use Blazor
dotnet add package DotNetQuery.Blazor
Setting Up the Client
With Dependency Injection (recommended)
In your Program.cs (or wherever you configure services), call AddDotNetQuery:
builder.Services.AddDotNetQuery();
That is the minimal setup. You can also configure the global defaults:
builder.Services.AddDotNetQuery(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
});
The registered IQueryClient has a Singleton lifetime by default (correct for WebAssembly / CSR apps). See the Server-Side Rendering guide if you are building a Blazor Server or SSR app.
Without Dependency Injection
If you are not using a DI container, use QueryClientFactory directly:
IQueryClient client = QueryClientFactory.Create(new QueryClientOptions
{
StaleTime = TimeSpan.FromMinutes(1),
});
Remember to call client.Dispose() when you are done with it.
Your First Query
A query needs two things: a key factory and a fetcher.
public sealed class UserQueries(IQueryClient queryClient, HttpClient httpClient) : IDisposable
{
public readonly IQuery<int, UserDto> UserQuery = queryClient.CreateQuery(
new QueryOptions<int, UserDto>
{
KeyFactory = id => QueryKey.From("users", id),
Fetcher = (id, ct) => httpClient.GetFromJsonAsync<UserDto>($"/api/users/{id}", ct)
?? throw new InvalidOperationException("User not found."),
}
);
public void Dispose() => UserQuery.Dispose();
}
Queries do not fetch anything on their own — they wait for you to push args:
// Trigger a fetch for user 42
userQueries.UserQuery.SetArgs(42);
Then subscribe to the state stream to react to changes:
userQueries.UserQuery.State.Subscribe(state =>
{
if (state.IsFetching)
ShowSpinner();
if (state.IsSuccess)
Render(state.CurrentData!);
if (state.IsFailure)
ShowError(state.Error!.Message);
});
Or use the shortcut streams if you only care about success or failure:
userQueries.UserQuery.Success.Subscribe(user => Console.WriteLine($"Got user: {user.Name}"));
userQueries.UserQuery.Failure.Subscribe(error => Console.WriteLine($"Failed: {error.Message}"));
You can also read the current state synchronously at any time — useful for rendering without subscribing:
var state = userQueries.UserQuery.CurrentState;
Your First Mutation
A mutation needs a mutator function:
public sealed class UserMutations(IQueryClient queryClient, HttpClient httpClient) : IDisposable
{
public readonly IMutation<CreateUserRequest, UserDto> CreateUser =
queryClient.CreateMutation(new MutationOptions<CreateUserRequest, UserDto>
{
Mutator = (request, ct) => httpClient.PostAsJsonAsync<UserDto>("/api/users", request, ct),
// 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();
}
Trigger the mutation by calling Execute:
userMutations.CreateUser.Execute(new CreateUserRequest { Name = "Alice" });
Subscribe to the mutation state the same way you would a query:
userMutations.CreateUser.State.Subscribe(state =>
{
if (state.IsRunning) ShowSpinner();
if (state.IsSuccess) NavigateTo("/users");
if (state.IsFailure) ShowError(state.Error!.Message);
});
Putting It Together in Blazor
If you are using Blazor, the <Suspense> and <Transition> components handle all the state-switching for you. Register the service classes and inject them into your components — components stay focused on rendering:
// Program.cs
builder.Services.AddScoped<UserQueries>();
builder.Services.AddScoped<UserMutations>();
Render query state declaratively with <Suspense>:
@inject UserQueries Queries
<Suspense Query="Queries.UserQuery">
<Content Context="user">
<p>Hello, @user.Name!</p>
</Content>
<Loading>
<p>Loading...</p>
</Loading>
<Failure Context="error">
<p>Something went wrong: @error.Message</p>
</Failure>
</Suspense>
@code {
protected override void OnInitialized()
{
Queries.UserQuery.SetArgs(42);
}
}
Subscribe to mutation state directly for form handling. The component only disposes its own subscription — the mutation itself is owned by the injected service:
@inject UserMutations Mutations
@inject NavigationManager Nav
@implements IDisposable
<button @onclick="HandleCreate">Create Alice</button>
@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();
}
See the Blazor Components guide for more details, including the <Transition> component for stale-while-revalidate rendering.
Next Steps
- Queries guide — everything about query configuration, lifecycle, and control.
- Mutations guide — deep dive into mutations, callbacks, and invalidation.
- Caching guide — understand stale time, cache time, and deduplication.
- Blazor Components guide —
<Suspense>and<Transition>in detail. - Observability guide — distributed tracing, metrics, and structured logging with OpenTelemetry.
- Examples — complete real-world examples.