Phetch.Core 0.3.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Phetch.Core --version 0.3.1
NuGet\Install-Package Phetch.Core -Version 0.3.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Phetch.Core" Version="0.3.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Phetch.Core --version 0.3.1
#r "nuget: Phetch.Core, 0.3.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Phetch.Core as a Cake Addin
#addin nuget:?package=Phetch.Core&version=0.3.1

// Install Phetch.Core as a Cake Tool
#tool nuget:?package=Phetch.Core&version=0.3.1

Phetch

Phetch is a small Blazor library for handling async query state, in the style of React Query, SWR, or RTK Query.

Currently, Phetch is only designed for use with Blazor WebAssembly. However, the core package (Phetch.Core) has no dependencies on Blazor or ASP.NET Core, so in theory it can be used anywhere that supports .NET Standard 2.1.

⚠️ Note: Phetch is in early development and likely to change.

Features

  • Automatically handles loading and error states, and updates your components whenever the state changes
  • Automatically caches data returned by queries, and makes it easy to invalidate or update this cached data when needed.
  • Supports any async method as a query or mutation (not restricted just to HTTP requests)
  • Built-in support for CancellationTokens
  • Supports mutations, dependent queries, pagination, prefetching, and request de-duplication
  • 100% strongly typed, with nullability annotations
  • Super lightweight and easy to mix-and-match with other state management methods
  • No Javascript whatsoever!

Show me some code!

Click here to view the source code for the sample project, with more detailed examples.

Below is the code for a basic component that runs a query when the component is first loaded. Phetch can do a whole lot more than that though, so make sure to check out the samples project and full documentation!

Defining an endpoint:

using Phetch.Core;

// This defines an endpoint that takes an int and returns a bool.
var isEvenEndpoint = new Endpoint<int, bool>(
    // Replace this part with your own async function:
    async (value, cancellationToken) =>
    {
        var response = await httpClient.GetFromJsonAsync<dynamic>(
            $"https://api.isevenapi.xyz/api/iseven/{value}",
            cancellationToken);
        return response.IsEven;
    }
);

Using the endpoint in a component:

@using Phetch.Blazor

<UseEndpoint Endpoint="isEvenEndpoint" Arg="3" Context="query">
    @if (query.IsError) {
        <p><em>Something went wrong!</em></p>
    } else if (query.IsLoading) {
        <p><em>Loading...</em></p>
    } else if (query.HasData) {
        <b>The number is @(query.Data ? "even" : "odd")</b>
    }
</UseEndpoint>

Some notes on the example above:

  • Inside the <UseEndpoint> component, you can use query to access the current state of the query. Changing the Context parameter will rename this object.
  • By changing the Arg parameter, the query will automatically be re-fetched when needed.
  • Normally, you would share endpoints around your application using dependency injection (see Defining Query Endpoints).
  • If you need to access the query state inside the @code block of a component, you can replace <UseEndpoint/> with the pattern described in Using Query Objects Directly.

Installing

You can install Phetch via the .NET CLI with the following command:

dotnet add package Phetch.Blazor

If you're using Visual Studio, you can also install via the built-in NuGet package manager.

Contributing

Any contributions are welcome, but ideally start by creating an issue.

Comparison with other libraries

  • Fluxor, Blazor-State or Cortex.Net: These are general-purpose state management libraries, so:
    • They will give you more control over exactly how your state is updated, and will work for managing non-query state.
    • However, for managing query state, you will need much more code to achieve the same things that Phetch can do in just a couple of lines. This becomes particularly important if you need to cache data or share queries across components.
  • Fusion: This is a much larger library, focused on real-time updates. Fusion is a game-changer if your app has lots of real-time functionality, but for most applications it will probably be overkill compared to Phetch.

Usage

There are a few different ways to define and use queries, depending on your use case.

In most cases, the best way to define queries is to use the Endpoint class. All components that use the same endpoint will share the same cache automatically.

// This defines an endpoint that takes an int and returns a bool.
var isEvenEndpoint = new Endpoint<int, bool>(
    // Replace this part with your own async function:
    async (value, cancellationToken) =>
    {
        var response = await httpClient.GetFromJsonAsync<dynamic>(
            $"https://api.isevenapi.xyz/api/iseven/{value}",
            cancellationToken);
        return response.IsEven;
    }
);

You can then share this instance of Endpoint across your whole application and use it wherever you need it. In most cases, the best way to do this is with Blazor's built-in dependency injection. You can view the sample project for a full example of how to do this, or follow the steps below:

<details> <summary>Setting up dependency injection (DI)</summary>

  1. Create a class containing an instance of Endpoint. You can have as many or few endpoints in a class as you want.
public class MyApi
{
    // An endpoint to retrieve a thing based on its ID
    public Endpoint<int, Thing> GetThingEndpoint { get; }

    // If your code has dependencies on other services (e.g., HttpClient),
    // you can add them as constructor parameters.
    public MyApi(HttpClient httpClient)
    {
        GetThingEndpoint = new(
            // TODO: Put your query function here.
        );
    }
}
  1. In Program.cs, add the following line (this might vary depending on the template you used):
builder.Services.AddScoped<MyApi>();
  1. To use the service, inject it in a component with the [Inject] attribute:
@inject MyApi Api

</details>

Creating Queries without Endpoints

If you just need to run a query in a single component and don't want to create an Endpoint, another option is to create a Query object directly.

var query = new Query<string, int>((id, cancellationToken) => ...);

⚠️ Unlike with Endpoints, you generally shouldn't share a single instance of Query across multiple components.

Using Query Endpoints with <UseEndpoint/>

Once you've defined a query endpoint, the best way to use it (in most cases) is with the <UseEndpoint /> Blazor component. This will handle re-rending the component automatically when the data changes.

If you provide the Arg parameter, this will also automatically request new data when the argument changes.

ℹ️ With <UseParameterlessEndpoint/>, use the AutoFetch parameter instead of passing an Arg.

// This assumes you have created a class called MyApi containing your endpoints,
// and registered it as a singleton or scoped service for dependency injection.
@inject MyApi Api

<UseEndpoint Endpoint="@Api.GetThing" Arg="ThingId" Context="query">
    @if (query.HasData)
    {
        <p>Thing Name: @query.Data.Name</p>
    }
    else if (query.IsLoading)
    {
        <p>Loading...</p>
    }
    else if (query.IsError)
    {
        <p>Error: @query.Error.Message</p>
    }
</UseEndpoint>

@code {
    [Parameter] public int ThingId { get; set; }
}

For a full working example, view the sample project.

Using Query objects directly

In cases where the <UseEndpoint/> component doesn't provide enough control, you can also use Query objects directly in your code. This is also useful when using endpoints or queries inside DI services.

Phetch.Blazor includes the <ObserveQuery/> component for this purpose, so that components can automatically be re-rendered when the query state changes. This automatically un-subscribes from query events when the component is unmounted (in Dispose()), so you don't have to worry about memory leaks.

ℹ️ Alternatively, you can manually subscribe and un-subscribe to a query using the StateChanged event.

@inject MyApi Api

@{ query.SetArg(ThingId) }
<ObserveQuery Query="query" OnChanged="StateHasChanged">

@if (query.HasData)
{
    // etc...
}

@code {
    private Query<int, Thing> query = null!;
    [Parameter] public int ThingId { get; set; }

    protected override void OnInitialized()
    {
        query = Api.GetThing.Use();
    }
}

Multiple Parameters

You will often need to define queries or mutations that accept multiple parameters (e.g., a search term and a page number). To do this, you can combine all the parameters into a tuple, like so:

var endpoint = new Endpoint<(string searchTerm, int page), List<string>>(
    (args, ct) => GetThingsAsync(args.searchTerm, args.page, ct)
)

For cases with lots of parameters, it is recommended to combine them into a record instead. This will allow you to define default values and other functionality.

Mutations and Parameterless Queries

Sometimes you will need to use query functions that either have no parameters, or no return value. In the case of queries without a return value, these are called Mutations. There is also a corresponding class called MutationEndpoint (as opposed to the normal Endpoint), and a <UseMutationEndpoint/> component (as opposed to <UseEndpoint/>), which are all designed to work with mutations.

ℹ️ Unlike some other libraries (e.g., React Query and RTK Query), mutations in Phetch behave exactly the same as queries (except for having no return value).

Equivalently, you can use the ParameterlessEndpoint class for query functions with no parameters.

Invoking queries manually

When you use <UseEndpoint/> or <UseMutationEndpoint/> endpoint and provide an Arg, the query will be fetched automatically, using data from the cache if available (see documentation).

However, you will sometimes need to control exactly when a query is run. A common use case for this is making requests that modify data on the server (e.g., PUT/POST requests).

The Query class contains four different methods for manually invoking queries, depending on your needs:

  1. SetArg: This updates the query argument, and automatically re-fetches the query if the argument has changed. If the same argument is passed multiple times in a row, the query will not be re-fetched.
  2. Refetch: This re-fetches the query using the last query argument passed via SetArg.
  3. Trigger: This always runs the query using the passed argument, regardless of whether cached data is available. Importantly, this will not share cached data with other components. This is the recommended way to run mutations in most cases.
  4. Invoke: This simply calls the original query function, completely ignoring all Phetch functionality (caching and state management).

Invalidation and Pessimistic Updates

Often, it will be useful to invalidate or update the data from other queries when a query or mutation completes.

To invalidate query data, you can use the Invalidate() methods on an Endpoint. This will cause the affected queries to be automatically re-fetched if they are currently being used. If they aren't being used, the cached data will be marked as invalidated, and then it will automatically re-fetch if it ever gets used.

Instead of invalidating data, you can also update the cached data directly using Endpoint.UpdateQueryData().

public class ExampleApi
{
    // An endpoint to retrieve a thing based on its ID
    public Endpoint<int, Thing> GetThingEndpoint { get; }

    // An endpoint with one parameter (the updated thing) and no return
    public MutationEndpoint<Thing> UpdateThingEndpoint { get; }

    public ExampleApi()
    {
        GetThingEndpoint = new(GetThingByIdAsync);

        UpdateThingEndpoint = new(UpdateThingAsync, options: new()
        {
            // Automatically invalidate the cached value for this Thing in GetThingEndpoint,
            // every time this mutation succeeds.
            OnSuccess = eventArgs => GetThingEndpoint.Invalidate(eventArgs.Arg.Id)
        });
    }

    async Task UpdateThingAsync(Thing thing, CancellationToken ct)
    {
        // TODO: Make an HTTP request to update thing
    }

    async Task<Thing> GetThingByIdAsync(int thingId, CancellationToken ct)
    {
        // TODO: Make an HTTP request to get thing
    }

    record Thing(int Id, string Name);
}

Cancellation

Queries that are currently running can be cancelled by calling query.Cancel(). This immediately resets the state of the query to whatever it was before the query was started.

This also cancels the CancellationToken that was passed to the query function, but this only has an effect if you used the CancellationToken in your query function. If you pass the CancellationToken to the HTTP client (see the code sample in Defining Query Endpoints), the browser will automatically cancel the in-flight request when you call query.Cancel.

It is still up to your API to correctly handle the cancellation, so you should not rely on this to cancel requests that modify data on the server.

Pre-fetching

If you know which data your user is likely to request in the future, you can call endpoint.Prefetch(arg) to trigger a request ahead of time and store the result in the cache. For example, you can use this to automatically fetch the next page of data in a table, which is demonstrated in the sample project.

If you already know what the query data will be, you can use endpoint.UpdateQueryData(arg, data) to add or update a cache entry without needing to run the query again.

Retries

To improve the resilience of your application, you can configure Phetch to automatically retry failed queries, using the RetryHandler option. If you just want to retry a fixed number of times, you can use RetryHandler.Simple like so:

var isEvenEndpoint = new Endpoint<int, bool>(
    GetIsEvenAsync, // Put you query function here
    options: new()
    {
        // Retry a maximum of two times on any exception (except cancellation)
        RetryHandler = RetryHandler.Simple(2),
    }
);

If you need to override the default retry behaviour of an endpoint in a single component, you can just pass a new RetryHandler to the options of endpoint.Use. To remove an existing retry handler entirely, use RetryHandler.None.

For more advanced use-cases, you can create your own class that implements IRetryHandler. See the implementation of SimpleRetryHandler for an example of how to do this. Alternatively, you can integrate Phetch with Polly as shown below:

<details> <summary>Integrating Phetch with Polly for advanced retries</summary>

Start by adding the following adapter class anywhere in your project:

using Phetch.Core;
using Polly;

public sealed record PollyRetryHandler(IAsyncPolicy Policy) : IRetryHandler
{
    public Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> queryFn, CancellationToken ct) =>
        Policy.ExecuteAsync(queryFn, ct);
}

Then, you can pass an instance of PollyRetryHandler to you endpoint or query:

var policy = Policy
    .Handle<HttpRequestException>()
    .RetryAsync(retryCount);

var endpointOptions = new EndpointOptions
{
    RetryHandler = new PollyRetryHandler(policy)
};

</details>

Sharing options between endpoints

Phetch does not directly provide a way to set the "default" options across all endpoints. Instead, you can create an instance of EndpointOptions with the default settings you want, and then manually use or extend this in each of your endpoints.

var defaultEndpointOptions = new EndpointOptions
{
    CacheTime = TimeSpan.FromMinutes(2),
    OnFailure = event => Console.WriteLine(event.Exception.Message),
    RetryHandler = RetryHandler.Simple(2),
};

You can then pass this directly to an Endpoint constructor, or pass it to the constructor of a new EndpointOptions to make a modified copy.

var isEvenEndpoint = new Endpoint<int, bool>(
    GetIsEvenAsync, // Put your query function here
    options: new(defaultEndpointOptions)
    {
        CacheTime = TimeSpan.FromMinutes(10),
    }
);

ℹ️ The difference between EndpointOptions and EndpointOptions<TArg, TResult> is intentional. Endpoint constructors can accept either, but you will need to use EndpointOptions<TArg, TResult> if you want to access the Arg and Result properties in the OnSuccess and OnFailure callbacks.

Endpoint options are immutable, so it is safe (and recommended) to make your "default" options instance static.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Phetch.Core:

Package Downloads
Phetch.Blazor

A small Blazor library for handling async query state, in the style of React Query

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.6.0 529 12/1/2023
0.5.1 320 7/14/2023
0.5.0 193 6/19/2023
0.4.0 306 3/6/2023
0.3.1 407 11/19/2022
0.3.0 419 11/13/2022
0.2.0 509 8/18/2022
0.1.2 530 8/6/2022
0.1.1 523 8/1/2022
0.1.0 522 7/26/2022
0.0.3 543 7/9/2022