Mutations

Eine Mutation<T> gibt Schreibvorgängen dieselbe bindbare Behandlung wie Lesevorgängen — IsLoading, IsError, ErrorMessage, Data — plus Cache-Invalidierung bei Erfolg.

Eine Mutation ausführen#

private readonly Mutation<Todo> _addTodo = new();

private Task Add() => _addTodo.Execute(
    token => Api.AddTodoAsync(_title, token),   // cancellation-aware shape
    p =>
    {
        p.OnSuccess = created => _title = "";
        p.OnError = e => _error = e.Message;
    });

Vier Funktionsformen#

MutationParameters<T> akzeptiert genau eine von vier Funktionen:

Funktion TypBeschreibung
MutationFunction Func<Task<T>>Gibt einen Wert zurück; die einfachste Form.
VoidMutationFunction Func<Task>Kein Rückgabewert; Data bleibt unberührt.
CancellableMutationFunction Func<CancellationToken, Task<T>>Gibt einen Wert zurück und beachtet das Lifetime-Token der Komponente — ein laufender Schreibvorgang wird aufgegeben, wenn die Komponente disposed wird.
CancellableVoidMutationFunction Func<CancellationToken, Task>Abbruchfähig, kein Rückgabewert.

Rangfolge, wenn mehrere gesetzt sind: CancellableMutationFunction gewinnt gegen MutationFunction, die wiederum gegen die void-Formen gewinnt. Nur die wertliefernden Formen schreiben Data — eine void-Mutation lässt ein früheres Ergebnis stehen. Wird keine von ihnen angegeben, wirft das eine ArgumentException.

InvalidateKeys#

Die in InvalidateKeys aufgeführten Keys werden — per Präfix-Abgleich — invalidiert, nachdem die Mutation erfolgreich war und OnSuccess gelaufen ist; ein Callback, der das frische Ergebnis beobachtet, läuft also, bevor abhängige Queries mit dem Refetchen beginnen. Jede gemountete Query unter diesen Keys lädt im Hintergrund neu, in jeder Komponente, die diese Daten anzeigt. Das ersetzt manuelle OnSuccessAsync = _ => Reload()-Verdrahtung.

p.InvalidateKeys = [QueryKey.Of("todos")];
// invalidates ["todos"], ["todos","list",1], ["todos","detail",42], …
Braucht den Cache

InvalidateKeys funktioniert über den registrierten DejaClient. Ohne AddDeja() werden die Keys stillschweigend ignoriert.

Folge-Requests verketten#

InvalidateKeys deckt „aktualisiere danach diese Keys“ ab. Wenn Key oder Argumente des Folge-Requests aus dem Mutationsergebnis stammen — oder wenn ein Request vom Ausgang eines anderen abhängt — verkette stattdessen über OnSuccessAsync. Es wird awaited, jeder Schritt wartet also auf den vorherigen, und das eigene Execute der Mutation ist erst abgeschlossen, wenn die Kette durch ist.

await _createOrder.Execute(() => Api.CreateOrderAsync(_draft), p => p.OnSuccessAsync = async order =>
{
    // The follow-up key comes from the result, which InvalidateKeys cannot express
    await _lines.Execute(QueryKey.Of("orders", order!.Id, "lines"),
        t => Api.GetOrderLinesAsync(order.Id, t),
        o => o.OnError = e => _error = e.Message);

    if (_lines.Data is { Count: 0 })
    {
        await _notify.Execute(() => Api.FlagEmptyOrderAsync(order.Id));
    }
});

Die Reihenfolge zwischen beiden ist fest: Zuerst läuft OnSuccess, dann InvalidateKeys (dessen Refetches awaited werden), dann OnSettled — ein Settled-Callback sieht die invalidierten Queries also bereits neu geladen. So verkettete Queries folgen denselben Regeln wie in Queries: Gib jedem Schritt seinen eigenen Error-Callback, damit ein Fehlschlag dem Request zugeordnet wird, der ihn verursacht hat.

Der Vertrag für unbehandelte Fehlschläge#

Bei einem Fehlschlag veröffentlicht die Mutation ihren Fehlerzustand und führt die Error-Callbacks aus. Dann kommt der Teil, den man sich merken sollte: eine InvalidOperationException, die die ursprüngliche Exception umhüllt, wird nur geworfen, wenn kein Error-Callback angegeben war. Ein Fehlschlag, den niemand beobachtet, geht nie stillschweigend verloren — verdrahte aber irgendeinen Error-Callback (OnError genügt), und der Fehlschlag bleibt behandelt, statt in den Komponenten-Lebenszyklus zu entkommen, wo ein Rethrow aus einem Event-Handler die Komponente wegen eines gewöhnlichen fehlgeschlagenen Schreibvorgangs abreißen würde.

Abbruch ist kein Fehlschlag: Wird die Komponente mitten im Schreibvorgang disposed, gibt es keinen Fehlerzustand, keine Callbacks und nichts, das einem Aufrufer entgegengeworfen wird, der längst weg ist. Eine Mutation, die nach dem Dispose gestartet wird (ein eingereihter Handler im Race mit dem Teardown), kehrt zurück, ohne den Request abzusetzen.

Live-Demo#

Erstellen + invalidieren
Create mutation IsLoading IsError Data: null
List query (invalidated on success) IsLoading IsReFetching IsError IsCachedData IsStale ReFetchCount: 1UpdatedAt: 20:33:27
  • delectus aut autem
  • quis ut nam facilis et officia qui
  • fugiat veniam minus
  • et porro tempora
@inherits DejaComponentBase
@inject JsonPlaceholderApi Api

<div class="demo-toolbar">
    <input class="demo-input" style="flex:1" placeholder="Todo title" @bind="_title" @bind:event="oninput"/>
    <button class="demo-button primary" @onclick="Create"
            disabled="@(_create.IsLoading || string.IsNullOrWhiteSpace(_title))">
        @(_create.IsLoading ? "Creating…" : "Create todo")
    </button>
</div>

<MutationInspector Mutation="_create" Label="Create mutation"/>
<StateInspector Query="_todos" Label="List query (invalidated on success)"/>

@if (_create.Data is { } created)
{
    <div class="demo-panel">
        <h4>Server response</h4>
        <p class="demo-note">Id @created.Id — “@created.Title” (JSONPlaceholder fakes the write, so the list refetch won't contain it)</p>
    </div>
}

@if (_create.IsError)
{
    <div class="demo-error">@_create.ErrorMessage</div>
}

@if (_todos.Data is { } todos)
{
    <ul class="demo-list">
        @foreach (var todo in todos.Take(4))
        {
            <li>@todo.Title</li>
        }
    </ul>
}

@code {
    private readonly Query<IReadOnlyList<TodoDto>> _todos = new();
    private readonly Mutation<TodoDto> _create = new();
    private string _title = string.Empty;

    protected override Task OnInitializedAsync()
        => _todos.Execute(DocsKeys.TodoList(4), token => Api.GetTodosAsync(4, token), p => p.OnError = _ => { });

    private Task Create() => _create.Execute(
        token => Api.CreateTodoAsync(new NewTodo(1, _title.Trim(), false), token),
        p =>
        {
            // After OnSuccess, every mounted query under the "todos" prefix refetches in the
            // background — watch IsReFetching light up on the list query below.
            p.InvalidateKeys = [DocsKeys.Todos];
            p.OnSuccess = _ => _title = string.Empty;
            p.OnError = _ => { };
        });
}