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 | Typ | Beschreibung |
|---|---|---|
| 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], …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#
- 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 = _ => { };
});
}