Verwenden der Azure Mobile Apps-Clientbibliothek für .NET

Dieser Leitfaden beschreibt gängige Szenarien für die Verwendung der .NET-Clientbibliothek für Azure Mobile Apps. Verwenden Sie die .NET-Clientbibliothek in einer beliebigen .NET 6- oder .NET Standard 2.0-Anwendung, einschließlich MAUI, Xamarin und Windows (WPF, UWP und WinUI).

Wenn Sie mit Azure Mobile Apps noch nicht vertraut sind, sollten Sie zunächst eines der Schnellstartlernprogramme abschließen:

Hinweis

In diesem Artikel wird die neueste Version (v6.0) des Microsoft Datasync Framework behandelt. Ältere Clients finden Sie in der v4.2.0-Dokumentation.

Unterstützte Plattformen

Die .NET-Clientbibliothek unterstützt jede .NET Standard 2.0- oder .NET 6-Plattform, einschließlich:

  • .NET MAUI für Android-, iOS- und Windows-Plattformen.
  • Android-API-Ebene 21 und höher (Xamarin und Android für .NET).
  • iOS Version 12.0 und höher (Xamarin und iOS für .NET).
  • Universelle Windows-Plattform Builds 19041 und höher.
  • Windows Presentation Framework (WPF).
  • Windows App SDK (WinUI 3).
  • Xamarin.Forms

Darüber hinaus wurden Beispiele für Avalonia und Uno Platform erstellt. Das TodoApp-Beispiel enthält ein Beispiel für jede getestete Plattform.

Einrichtung und Voraussetzungen

Fügen Sie die folgenden Bibliotheken aus NuGet hinzu:

Wenn Sie ein Plattformprojekt (z. B. .NET MAUI) verwenden, stellen Sie sicher, dass Sie die Bibliotheken zum Plattformprojekt und allen freigegebenen Projekten hinzufügen.

Erstellen des Dienstclients

Der folgende Code erstellt den Dienstclient, der verwendet wird, um die gesamte Kommunikation mit den Back-End- und Offlinetabellen zu koordinieren.

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", options);

Ersetzen Sie MOBILE_APP_URL im vorherigen Code durch die URL des ASP.NET Core-Back-Ends. Der Client sollte als Singleton erstellt werden. Wenn Sie einen Authentifizierungsanbieter verwenden, kann er wie folgt konfiguriert werden:

var options = new DatasyncClientOptions 
{
    // Options set here
};
var client = new DatasyncClient("MOBILE_APP_URL", authProvider, options);

Weitere Details zum Authentifizierungsanbieter werden weiter unten in diesem Dokument bereitgestellt.

Tastatur

Eine vollständige (Standardeinstellung) von Optionen kann wie folgt erstellt werden:

var options = new DatasyncClientOptions
{
    HttpPipeline = new HttpMessageHandler[](),
    IdGenerator = (table) => Guid.NewGuid().ToString("N"),
    InstallationId = null,
    OfflineStore = null,
    ParallelOperations = 1,
    SerializerSettings = null,
    TableEndpointResolver = (table) => $"/tables/{tableName.ToLowerInvariant()}",
    UserAgent = $"Datasync/5.0 (/* Device information */)"
};

HttpPipeline

Normalerweise erfolgt eine HTTP-Anforderung durch Übergeben der Anforderung über den Authentifizierungsanbieter (der den Authorization Header für den aktuell authentifizierten Benutzer hinzufügt), bevor die Anforderung gesendet wird. Sie können optional weitere Delegierungshandler hinzufügen. Jede Anforderung durchläuft die delegierenden Handler, bevor sie an den Dienst gesendet werden. Durch Delegieren von Handlern können Sie zusätzliche Header hinzufügen, Wiederholungen durchführen oder Protokollierungsfunktionen bereitstellen.

Beispiele für das Delegieren von Handlern werden für die Protokollierung und das Hinzufügen von Anforderungsheadern weiter unten in diesem Artikel bereitgestellt.

IdGenerator

Wenn einer Offlinetabelle eine Entität hinzugefügt wird, muss sie über eine ID verfügen. Wenn keine ID angegeben wird, wird eine ID generiert. Mit IdGenerator der Option können Sie die generierte ID anpassen. Standardmäßig wird eine global eindeutige ID generiert. Die folgende Einstellung generiert beispielsweise eine Zeichenfolge, die den Tabellennamen und eine GUID enthält:

var options = new DatasyncClientOptions 
{
    IdGenerator = (table) => $"{table}-{Guid.NewGuid().ToString("D").ToUpperInvariant()}"
}

InstallationId

Wenn ein InstallationId Satz festgelegt ist, wird mit jeder Anforderung eine benutzerdefinierte Kopfzeile X-ZUMO-INSTALLATION-ID gesendet, um die Kombination der Anwendung auf einem bestimmten Gerät zu identifizieren. Dieser Header kann in Protokollen aufgezeichnet werden und ermöglicht es Ihnen, die Anzahl der unterschiedlichen Installationen für Ihre App zu ermitteln. Wenn Sie die ID verwenden InstallationId, sollte die ID im beständigen Speicher auf dem Gerät gespeichert werden, damit eindeutige Installationen nachverfolgt werden können.

OfflineStore

Dies OfflineStore wird beim Konfigurieren des Offlinedatenzugriffs verwendet. Weitere Informationen finden Sie unter "Arbeiten mit Offlinetabellen".

ParallelOperations

Ein Teil des Offlinesynchronisierungsprozesses umfasst Pushvorgänge in der Warteschlange an den Remoteserver. Wenn der Pushvorgang ausgelöst wird, werden die Vorgänge in der Reihenfolge übermittelt, in der sie empfangen wurden. Sie können optional bis zu acht Threads verwenden, um diese Vorgänge zu übertragen. Parallele Vorgänge verwenden mehr Ressourcen sowohl auf Dem Client als auch auf dem Server, um den Vorgang schneller abzuschließen. Die Reihenfolge, in der Vorgänge auf dem Server ankommen, kann nicht garantiert werden, wenn mehrere Threads verwendet werden.

Serialisierer Einstellungen

Wenn Sie die Serialisierungseinstellungen auf dem Datensynchronisierungsserver geändert haben, müssen Sie dieselben Änderungen am SerializerSettings Client vornehmen. Mit dieser Option können Sie Ihre eigenen Serialisierungseinstellungen angeben.

TableEndpointResolver

Standardmäßig befinden sich Tabellen auf dem Remotedienst im /tables/{tableName} Pfad (wie durch das Route Attribut im Servercode angegeben). Tabellen können jedoch auf jedem Endpunktpfad vorhanden sein. Dies TableEndpointResolver ist eine Funktion, die einen Tabellennamen in einen Pfad für die Kommunikation mit dem Remotedienst verwandelt.

Die folgende Änderung ändert beispielsweise die Annahme, dass sich alle Tabellen unter /api:

var options = new DatasyncClientOptions
{
    TableEndpointResolver = (table) => $"/api/{table}"
};

Benutzer-Agent

Der Datensynchronisierungsclient generiert basierend auf der Version der Bibliothek einen geeigneten User-Agent-Headerwert. Einige Entwickler haben das Gefühl, dass der Benutzer-Agent-Header Informationen über den Client verleckt. Sie können die UserAgent Eigenschaft auf einen beliebigen gültigen Headerwert festlegen.

Arbeiten mit Remotetabellen

Im folgenden Abschnitt wird erläutert, wie Datensätze gesucht und abgerufen und die Daten in einer Remotetabelle geändert werden. Die folgenden Themen werden behandelt:

Erstellen eines Remotetabellenverweises

Verwenden Sie GetRemoteTable<T>folgendes, um einen Remotetabellenverweis zu erstellen:

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

Wenn Sie eine schreibgeschützte Tabelle zurückgeben möchten, verwenden Sie die IReadOnlyRemoteTable<T> Version:

IReadOnlyRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();

Der Modelltyp muss den ITableData Vertrag vom Dienst implementieren. Wird DatasyncClientData verwendet, um die erforderlichen Felder bereitzustellen:

public class TodoItem : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

Das DatasyncClientData Objekt umfasst:

  • Id (Zeichenfolge) – eine global eindeutige ID für das Element.
  • UpdatedAt (System.DataTimeOffset) – Datum/Uhrzeit, zu der das Element zuletzt aktualisiert wurde.
  • Version (zeichenfolge) - eine undurchsichtige Zeichenfolge, die für die Versionsverwaltung verwendet wird.
  • Deleted (boolescher Wert) – wenn true, wird das Element gelöscht.

Der Dienst Standard diese Felder enthält. Passen Sie diese Felder nicht als Teil Der Clientanwendung an.

Modelle können mit Newtonsoft.JSON-Attributen kommentiert werden. Der Name der Tabelle kann mithilfe des DataTable Attributs angegeben werden:

[DataTable("todoitem")]
public class MyTodoItemClass : DatasyncClientData
{
    public string Title { get; set; }
    public bool IsComplete { get; set; }
}

Alternativ können Sie den Namen der Tabelle im GetRemoteTable() Aufruf angeben:

IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable("todoitem");

Der Client verwendet den Pfad /tables/{tablename} als URI. Der Tabellenname ist auch der Name der Offlinetabelle in der SQLite-Datenbank.

Unterstützte Typen

Neben primitiven Typen (int, float, string usw.) werden die folgenden Typen für Modelle unterstützt:

  • System.DateTime - als ISO-8601 UTC-Datums-/Uhrzeitzeichenfolge mit ms-Genauigkeit.
  • System.DateTimeOffset - als ISO-8601 UTC-Datums-/Uhrzeitzeichenfolge mit ms-Genauigkeit.
  • System.Guid - als 32 Ziffern formatiert, die als Bindestriche getrennt sind.

Abfragen von Daten von einem Remoteserver

Die Remotetabelle kann mit LINQ-ähnlichen Anweisungen verwendet werden, einschließlich:

  • Filtern mit einer .Where() Klausel.
  • Sortieren mit verschiedenen .OrderBy() Klauseln.
  • Auswählen von Eigenschaften mit .Select().
  • Paging mit .Skip() und .Take().

Zählen von Elementen aus einer Abfrage

Wenn Sie eine Anzahl der Elemente benötigen, die die Abfrage zurückgeben würde, können Sie für eine Tabelle oder .LongCountAsync() für eine Abfrage verwenden.CountItemsAsync():

// Count items in a table.
long count = await remoteTable.CountItemsAsync();

// Count items in a query.
long count = await remoteTable.Where(m => m.Rating == "R").LongCountAsync();

Diese Methode verursacht einen Roundtrip zum Server. Sie können auch beim Auffüllen einer Liste (z. B. eine Zählung) abrufen, um den zusätzlichen Roundtrip zu vermeiden:

var enumerable = remoteTable.ToAsyncEnumerable() as AsyncPageable<T>;
var list = new List<T>();
long count = 0;
await foreach (var item in enumerable)
{
    count = enumerable.Count;
    list.Add(item);
}

Die Anzahl wird nach der ersten Anforderung zum Abrufen des Tabelleninhalts aufgefüllt.

Zurückgeben aller Daten

Daten werden über eine IAsyncEnumerable zurückgegeben:

var enumerable = remoteTable.ToAsyncEnumerable();
await foreach (var item in enumerable) 
{
    // Process each item
}

Verwenden Sie eine der folgenden Beendigungsklauseln, um die IAsyncEnumerable<T> Klausel in eine andere Auflistung zu konvertieren:

T[] items = await remoteTable.ToArrayAsync();

Dictionary<string, T> items = await remoteTable.ToDictionaryAsync(t => t.Id);

HashSet<T> items = await remoteTable.ToHashSetAsync();

List<T> items = await remoteTable.ToListAsync();

Im Hintergrund behandelt die Remotetabelle die Auslagerung des Ergebnisses für Sie. Alle Elemente werden unabhängig davon zurückgegeben, wie viele serverseitige Anforderungen erforderlich sind, um die Abfrage zu erfüllen. Diese Elemente sind auch für Abfrageergebnisse verfügbar (z. B remoteTable.Where(m => m.Rating == "R"). ).

Das Datensynchronisierungsframework bietet ConcurrentObservableCollection<T> auch eine threadsichere feststellbare Sammlung. Diese Klasse kann im Kontext von Benutzeroberflächenanwendungen verwendet werden, die normalerweise zum Verwalten einer Liste (z. B. Xamarin Forms oder MAUI-Listen) verwendet ObservableCollection<T> werden. Sie können eine ConcurrentObservableCollection<T> direkt aus einer Tabelle oder Abfrage löschen und laden:

var collection = new ConcurrentObservableCollection<T>();
await remoteTable.ToObservableCollection(collection);

Die Verwendung .ToObservableCollection(collection) des Ereignisses löst das CollectionChanged Ereignis einmal für die gesamte Auflistung und nicht für einzelne Elemente aus, was zu einer schnelleren Neuerfassungszeit führt.

Darüber hinaus gibt es ConcurrentObservableCollection<T> prädikatgesteuerte Änderungen:

// Add an item only if the identified item is missing.
bool modified = collection.AddIfMissing(t => t.Id == item.Id, item);

// Delete one or more item(s) based on a predicate
bool modified = collection.DeleteIf(t => t.Id == item.Id);

// Replace one or more item(s) based on a predicate
bool modified = collection.ReplaceIf(t => t.Id == item.Id, item);

Prädikatgesteuerte Änderungen können in Ereignishandlern verwendet werden, wenn der Index des Elements im Voraus nicht bekannt ist.

Filtern von Daten

Sie können eine .Where() Klausel verwenden, um Daten zu filtern. Beispiel:

var items = await remoteTable.Where(x => !x.IsComplete).ToListAsync();

Das Filtern erfolgt auf dem Dienst vor dem IAsyncEnumerable und auf dem Client nach dem IAsyncEnumerable-Element. Beispiel:

var items = (await remoteTable.Where(x => !x.IsComplete).ToListAsync()).Where(x => x.Title.StartsWith("The"));

Die erste .Where() Klausel (nur unvollständige Elemente zurückgeben) wird für den Dienst ausgeführt, während die zweite .Where() Klausel (beginnend mit "The") auf dem Client ausgeführt wird.

Die Where -Klausel unterstützt Vorgänge, die in die OData-Teilmenge übersetzt werden können. Folgende Vorgänge sind möglich:

  • Relationale Operatoren (==, !=, <, <=, >, >=),
  • Arithmetische Operatoren (+, -, /, *, %),
  • Zahlengenauigkeit (Math.Floor, Math.Ceiling),
  • Zeichenfolgenfunktionen (Length, Substring, , ReplaceIndexOf, Equals, StartsWith, ) EndsWith(nur ordinale und invariante Kulturen),
  • Datumseigenschaften (Year, Month, Day, Hour, Minute, Second),
  • Zugriff auf Eigenschaften eines Objekts
  • Ausdrücke, die beliebige dieser Vorgänge kombinieren

Sortieren von Daten

Verwenden Sie , , und .ThenByDescending() verwenden Sie .OrderBy()einen Eigenschaftsaccessor zum Sortieren von .ThenBy()Daten. .OrderByDescending()

var items = await remoteTable.OrderBy(x => x.IsComplete).ThenBy(x => x.Title).ToListAsync();

Die Sortierung erfolgt durch den Dienst. Sie können in keiner Sortierklausel einen Ausdruck angeben. Wenn Sie nach einem Ausdruck sortieren möchten, verwenden Sie die clientseitige Sortierung:

var items = await remoteTable.ToListAsync().OrderBy(x => x.Title.ToLowerCase());

Auswählen von Eigenschaften

Sie können eine Teilmenge von Daten aus dem Dienst zurückgeben:

var items = await remoteTable.Select(x => new { x.Id, x.Title, x.IsComplete }).ToListAsync();

Zurückgeben einer Datenseite

Sie können eine Teilmenge des Datasets mithilfe .Skip() und .Take() zum Implementieren von Paging zurückgeben:

var pageOfItems = await remoteTable.Skip(100).Take(10).ToListAsync();

In tatsächlichen Apps können Sie ähnliche Abfragen wie im vorherigen Beispiel mit einem Pagingsteuerelement oder einer vergleichbaren Benutzeroberfläche ausführen, um zwischen Seiten zu navigieren.

Alle bisher beschriebenen Funktionen sind additiv, wir können also damit fortfahren, sie zu verketten. Jeder verkettete Aufruf betrifft einen größeren Teil der Abfrage. Ein weiteres Beispiel:

var query = todoTable
                .Where(todoItem => todoItem.Complete == false)
                .Select(todoItem => todoItem.Text)
                .Skip(3).
                .Take(3);
List<string> items = await query.ToListAsync();

Nachschlagen von Remotedaten nach ID

Die GetItemAsync -Funktion kann verwendet werden, um Objekte mit einer bestimmten ID in der Datenbank zu suchen.

TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

Wenn das abzurufende Element vorläufig gelöscht wurde, müssen Sie den includeDeleted Parameter verwenden:

// The following code will throw a DatasyncClientException if the item is soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");

// This code will retrieve the item even if soft-deleted.
TodoItem item = await remoteTable.GetItemAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D", includeDeleted: true);

Einfügen von Daten auf dem Remoteserver

Alle Clienttypen müssen einen Member mit dem Namen Id enthalten. Dies ist standardmäßig eine Zeichenfolge. Diese ID ist zum Ausführen von CRUD-Vorgängen und für die Offlinesynchronisierung erforderlich. Der folgende Code veranschaulicht, wie die InsertItemAsync Methode verwendet wird, um neue Zeilen in eine Tabelle einzufügen. Der Parameter enthält die einzufügenden Daten als .NET-Objekt.

var item = new TodoItem { Title = "Text", IsComplete = false };
await remoteTable.InsertItemAsync(item);
// Note that item.Id will now be set

Wenn ein eindeutiger benutzerdefinierter ID-Wert während item eines Einfügens nicht enthalten ist, generiert der Server eine ID. Sie können die generierte ID abrufen, indem Sie das Objekt nach Rückgabe des Aufrufs untersuchen.

Aktualisieren von Daten auf dem Remoteserver

Im folgenden Code wird veranschaulicht, wie Sie mithilfe der ReplaceItemAsync Methode einen vorhandenen Datensatz mit derselben ID mit neuen Informationen aktualisieren.

// In this example, we assume the item has been created from the InsertItemAsync sample

item.IsComplete = true;
await remoteTable.ReplaceItemAsync(todoItem);

Löschen von Daten auf dem Remoteserver

Der folgende Code veranschaulicht, wie die DeleteItemAsync Methode zum Löschen einer vorhandenen Instanz verwendet wird.

// In this example, we assume the item has been created from the InsertItemAsync sample

await todoTable.DeleteItemAsync(item);

Konfliktauflösung und optimistische Parallelität

Zwei oder mehr Clients können Gleichzeitig Änderungen an demselben Element schreiben. Ohne Konflikterkennung würde der letzte Schreibvorgang alle vorherigen Aktualisierungen überschreiben. Optimistische Parallelitätssteuerung setzt voraus, dass jede Transaktion Commit ausführen kann und daher keine Ressourcensperre verwendet. Das optimistische Parallelitätssteuerelement überprüft, ob keine andere Transaktion die Daten geändert hat, bevor ein Commit für die Daten ausgeführt wird. Wenn die Daten geändert wurden, wird die Transaktion zurückgesetzt.

Azure Mobile Apps unterstützt optimistische Parallelitätssteuerung, indem Änderungen an den einzelnen Elementen mithilfe der Systemeigenschaftenspalte nachverfolgt werden, die version für jede Tabelle in Ihrem Mobile App-Back-End definiert ist. Bei jeder Aktualisierung eines Datensatzes wird die version -Eigenschaft des entsprechenden Datensatzes von Mobile Apps auf einen neuen Wert festgelegt. Bei jeder Aktualisierungsanforderung wird die version -Eigenschaft des in der Anforderung enthaltenen Datensatzes mit der Eigenschaft des Datensatzes auf dem Server verglichen. Wenn die mit der Anforderung übergebene Version nicht mit dem Back-End übereinstimmt, löst die Clientbibliothek eine DatasyncConflictException<T> Ausnahme aus. Der in der Ausnahme enthaltene Typ ist der Datensatz des Back-Ends, der die Serverversion des entsprechenden Datensatzes enthält. Anschließend kann die Anwendung anhand dieser Informationen entscheiden, ob die Updateanforderung erneut mit dem korrekten version -Wert vom Back-End ausgeführt werden soll, um Commits für die Änderungen auszuführen.

Optimistische Parallelität wird bei Verwendung des DatasyncClientData Basisobjekts automatisch aktiviert.

Zusätzlich zur Aktivierung optimistischer Parallelität müssen Sie auch die DatasyncConflictException<T> Ausnahme im Code abfangen. Beheben Sie den Konflikt, indem Sie den korrekten version Datensatz anwenden und dann den Anruf mit dem aufgelösten Datensatz wiederholen. Der folgende Code zeigt, wie ein erkannter Schreibkonflikt gelöst werden kann:

private async void UpdateToDoItem(TodoItem item)
{
    DatasyncConflictException<TodoItem> exception = null;

    try
    {
        //update at the remote table
        await remoteTable.UpdateAsync(item);
    }
    catch (DatasyncConflictException<TodoItem> writeException)
    {
        exception = writeException;
    }

    if (exception != null)
    {
        // Conflict detected, the item has changed since the last query
        // Resolve the conflict between the local and server item
        await ResolveConflict(item, exception.Item);
    }
}


private async Task ResolveConflict(TodoItem localItem, TodoItem serverItem)
{
    //Ask user to choose the resolution between versions
    MessageDialog msgDialog = new MessageDialog(
        String.Format("Server Text: \"{0}\" \nLocal Text: \"{1}\"\n",
        serverItem.Text, localItem.Text),
        "CONFLICT DETECTED - Select a resolution:");

    UICommand localBtn = new UICommand("Commit Local Text");
    UICommand ServerBtn = new UICommand("Leave Server Text");
    msgDialog.Commands.Add(localBtn);
    msgDialog.Commands.Add(ServerBtn);

    localBtn.Invoked = async (IUICommand command) =>
    {
        // To resolve the conflict, update the version of the item being committed. Otherwise, you will keep
        // catching a MobileServicePreConditionFailedException.
        localItem.Version = serverItem.Version;

        // Updating recursively here just in case another change happened while the user was making a decision
        UpdateToDoItem(localItem);
    };

    ServerBtn.Invoked = async (IUICommand command) =>
    {
        RefreshTodoItems();
    };

    await msgDialog.ShowAsync();
}

Arbeiten mit Offlinetabellen

Offlinetabellen verwenden eine lokalen SQLite-Speicher zum Speichern von Daten für die Offline-Verwendung. Alle Tabellenvorgänge werden gegen den lokalen SQLite-Speicher statt den Remote-Serverspeicher ausgeführt. Stellen Sie sicher, dass Sie jedes Microsoft.Datasync.Client.SQLiteStore Plattformprojekt und allen freigegebenen Projekten hinzufügen.

Bevor ein Tabellenverweis erstellt werden, muss der lokale Speicher vorbereitet werden:

var store = new OfflineSQLiteStore(Constants.OfflineConnectionString);
store.DefineTable<TodoItem>();

Nachdem der Speicher definiert wurde, können Sie den Client erstellen:

var options = new DatasyncClientOptions 
{
    OfflineStore = store
};
var client = new DatasyncClient("MOBILE_URL", options);

Schließlich müssen Sie sicherstellen, dass die Offlinefunktionen initialisiert werden:

await client.InitializeOfflineStoreAsync();

Die Speicherinitialisierung erfolgt normalerweise sofort nach dem Erstellen des Clients. Die Offline Verbinden ionString ist ein URI, der zum Angeben des Speicherorts der SQLite-Datenbank und der Optionen zum Öffnen der Datenbank verwendet wird. Weitere Informationen finden Sie unter URI-Dateinamen in SQLite.

  • Um einen Speichercache zu verwenden, verwenden Sie file:inmemory.db?mode=memory&cache=private.
  • Um eine Datei zu verwenden, verwenden Sie file:/path/to/file.db

Sie müssen den absoluten Dateinamen für die Datei angeben. Wenn Sie Xamarin verwenden, können Sie die Xamarin Essentials File System Helpers verwenden, um einen Pfad zu erstellen: Beispiel:

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

Wenn Sie MAUI verwenden, können Sie die MAUI-Dateisystemhilfsprogramme verwenden, um einen Pfad zu erstellen: Beispiel:

var dbPath = $"{Filesystem.AppDataDirectory}/todoitems.db";
var store = new OfflineSQLiteStore($"file:/{dbPath}?mode=rwc");

Erstellen einer Offlinetabelle

Ein Tabellenverweis kann mit der GetOfflineTable<T>-Methode:abgerufen werden:

IOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

Wie bei der Remotetabelle können Sie auch eine schreibgeschützte Offlinetabelle verfügbar machen:

IReadOnlyOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();

Sie müssen sich nicht authentifizieren, um eine Offlinetabelle zu verwenden. Sie müssen sich nur authentifizieren, wenn Sie mit dem Back-End-Dienst kommunizieren.

Synchronisieren einer Offlinetabelle

Offlinetabellen werden standardmäßig nicht mit dem Back-End synchronisiert. Die Synchronisierung ist in zwei Bereiche unterteilt. Sie können Änderungen separat vom Herunterladen neuer Elemente übertragen. Beispiel:

public async Task SyncAsync()
{
    ReadOnlyCollection<TableOperationError> syncErrors = null;

    try
    {
        foreach (var offlineTable in offlineTables.Values)
        {
            await offlineTable.PushItemsAsync();
            await offlineTable.PullItemsAsync("", options);
        }
    }
    catch (PushFailedException exc)
    {
        if (exc.PushResult != null)
        {
            syncErrors = exc.PushResult.Errors;
        }
    }

    // Simple error/conflict handling
    if (syncErrors != null)
    {
        foreach (var error in syncErrors)
        {
            if (error.OperationKind == TableOperationKind.Update && error.Result != null)
            {
                //Update failed, reverting to server's copy.
                await error.CancelAndUpdateItemAsync(error.Result);
            }
            else
            {
                // Discard local change.
                await error.CancelAndDiscardItemAsync();
            }

            Debug.WriteLine(@"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
        }
    }
}

Standardmäßig verwenden alle Tabellen die inkrementelle Synchronisierung – nur neue Datensätze werden abgerufen. Ein Datensatz ist für jede eindeutige Abfrage enthalten (generiert durch Erstellen eines MD5-Hashs der OData-Abfrage).

Hinweis

Das erste Argument PullItemsAsync ist die OData-Abfrage, die angibt, welche Datensätze auf das Gerät übertragen werden sollen. Es ist besser, den Dienst so zu ändern, dass nur datensätze zurückgegeben werden, die für den Benutzer spezifisch sind, anstatt komplexe Abfragen auf clientseitiger Seite zu erstellen.

Die vom Objekt definierten PullOptions Optionen müssen im Allgemeinen nicht festgelegt werden. Beispiele für Optionen:

  • PushOtherTables - Wenn dieser Wert auf "true" festgelegt ist, werden alle Tabellen verschoben.
  • QueryId - eine bestimmte Abfrage-ID, die anstelle der generierten id verwendet werden soll.
  • WriteDeltaTokenInterval – wie oft das Delta-Token geschrieben wird, das zum Nachverfolgen der inkrementellen Synchronisierung verwendet wird.

Das SDK führt vor dem Abrufen von Datensätzen eine implizite PushAsync() durch.

Die Konfliktbehandlung erfolgt über eine PullAsync()-Methode. Behandeln Sie Konflikte auf die gleiche Weise wie Onlinetabellen. Der Konflikt entsteht, wenn PullAsync() statt „insert“, „update“ oder „delete“ aufgerufen wird. Wenn mehrere Konflikte auftreten, werden sie in einem einzigen PushFailedExceptiongebündelt. Behandeln Sie jeden Fehler einzeln.

Pushänderungen für alle Tabellen

Um alle Änderungen an den Remoteserver zu übertragen, verwenden Sie Folgendes:

await client.PushTablesAsync();

Um Änderungen für eine Teilmenge von Tabellen zu übertragen, geben Sie eine IEnumerable<string> an die PushTablesAsync() Methode an:

var tablesToPush = new string[] { "TodoItem", "Notes" };
await client.PushTables(tablesToPush);

Verwenden Sie die client.PendingOperations Eigenschaft, um die Anzahl der Vorgänge zu lesen, die darauf warten, an den Remotedienst zu übertragen. Diese Eigenschaft ist null der Fall, wenn kein Offlinespeicher konfiguriert wurde.

Ausführen komplexer SQLite-Abfragen

Wenn Sie komplexe SQL-Abfragen für die Offlinedatenbank ausführen müssen, können Sie dies mithilfe der ExecuteQueryAsync() Methode tun. Wenn Sie z. B. eine SQL JOIN Anweisung ausführen möchten, definieren Sie eine JObject , die die Struktur des Rückgabewerts anzeigt, und verwenden Sie dann Folgendes ExecuteQueryAsync():

var definition = new JObject() 
{
    { "id", string.Empty },
    { "title", string.Empty },
    { "first_name", string.Empty },
    { "last_name", string.Empty }
};
var sqlStatement = "SELECT b.id as id, b.title as title, a.first_name as first_name, a.last_name as last_name FROM books b INNER JOIN authors a ON b.author_id = a.id ORDER BY b.id";

var items = await store.ExecuteQueryAsync(definition, sqlStatement, parameters);
// Items is an IList<JObject> where each JObject conforms to the definition.

Die Definition ist ein Satz von Schlüssel/Werten. Die Schlüssel müssen mit den Feldnamen übereinstimmen, die von der SQL-Abfrage zurückgegeben werden, und die Werte müssen der Standardwert des erwarteten Typs sein. Wird 0L für Zahlen (lang), false für Booleane und string.Empty für alles andere verwendet.

SQLite verfügt über einen restriktiven Satz unterstützter Typen. Datum/Uhrzeiten werden seit der Epoche als Anzahl von Millisekunden gespeichert, um Vergleiche zu ermöglichen.

Benutzer authentifizieren

Mit Azure Mobile Apps können Sie einen Authentifizierungsanbieter für die Behandlung von Authentifizierungsaufrufen generieren. Geben Sie beim Erstellen des Dienstclients den Authentifizierungsanbieter an:

AuthenticationProvider authProvider = GetAuthenticationProvider();
var client = new DatasyncClient("APP_URL", authProvider);

Wenn die Authentifizierung erforderlich ist, wird der Authentifizierungsanbieter aufgerufen, um das Token abzurufen. Ein generischer Authentifizierungsanbieter kann sowohl für die autorisierungsbasierte Authentifizierung als auch für die autorisierungsbasierte Authentifizierung und autorisierungsbasierte Authentifizierung verwendet werden. Verwenden Sie das folgende Modell:

public AuthenticationProvider GetAuthenticationProvider()
    => new GenericAuthenticationProvider(GetTokenAsync);

// Or, if using Azure App Service Authentication and Authorization
// public AuthenticationProvider GetAuthenticationProvider()
//    => new GenericAuthenticationProvider(GetTokenAsync, "X-ZUMO-AUTH");

public async Task<AuthenticationToken> GetTokenAsync()
{
    // TODO: Any code necessary to get the right access token.
    
    return new AuthenticationToken 
    {
        DisplayName = "/* the display name of the user */",
        ExpiresOn = DateTimeOffset.Now.AddHours(1), /* when does the token expire? */
        Token = "/* the access token */",
        UserId = "/* the user id of the connected user */"
    };
}

Authentifizierungstoken werden im Arbeitsspeicher zwischengespeichert (nie auf Das Gerät geschrieben) und bei Bedarf aktualisiert.

Verwenden der Microsoft Identity Platform

Mit der Microsoft Identity Platform können Sie problemlos in Microsoft Entra ID integriert werden. In den Schnellstartlernprogrammen finden Sie ein vollständiges Lernprogramm zum Implementieren der Microsoft Entra-Authentifizierung. Der folgende Code zeigt ein Beispiel für das Abrufen des Zugriffstokens:

private readonly string[] _scopes = { /* provide your AAD scopes */ };
private readonly object _parentWindow; /* Fill in with the required object before using */
private readonly PublicClientApplication _pca; /* Create one */

public MyAuthenticationHelper(object parentWindow) 
{
    _parentWindow = parentWindow;
    _pca = PublicClientApplicationBuilder.Create(clientId)
            .WithRedirectUri(redirectUri)
            .WithAuthority(authority)
            /* Add options methods here */
            .Build();
}

public async Task<AuthenticationToken> GetTokenAsync()
{
    // Silent authentication
    try
    {
        var account = await _pca.GetAccountsAsync().FirstOrDefault();
        var result = await _pca.AcquireTokenSilent(_scopes, account).ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex) when (exception is not MsalUiRequiredException)
    {
        // Handle authentication failure
        return null;
    }

    // UI-based authentication
    try
    {
        var account = await _pca.AcquireTokenInteractive(_scopes)
            .WithParentActivityOrWindow(_parentWindow)
            .ExecuteAsync();
        
        return new AuthenticationToken 
        {
            ExpiresOn = result.ExpiresOn,
            Token = result.AccessToken,
            UserId = result.Account?.Username ?? string.Empty
        };    
    }
    catch (Exception ex)
    {
        // Handle authentication failure
        return null;
    }
}

Weitere Informationen zur Integration der Microsoft Identity Platform in ASP.NET 6 finden Sie in der Dokumentation zur Microsoft Identity Platform .

Verwenden von Xamarin Essentials oder MAUI WebAuthenticator

Für Azure-App Dienstauthentifizierung können Sie den Xamarin Essentials WebAuthenticator oder den MAUI WebAuthenticator verwenden, um ein Token abzurufen:

Uri authEndpoint = new Uri(client.Endpoint, "/.auth/login/aad");
Uri callback = new Uri("myapp://easyauth.callback");

public async Task<AuthenticationToken> GetTokenAsync()
{
    var authResult = await WebAuthenticator.AuthenticateAsync(authEndpoint, callback);
    return new AuthenticationToken 
    {
        ExpiresOn = authResult.ExpiresIn,
        Token = authResult.AccessToken
    };
}

Die UserId und DisplayName sind nicht direkt verfügbar, wenn sie Azure-App Dienstauthentifizierung verwenden. Verwenden Sie stattdessen einen faulen Anforderer, um die Informationen vom /.auth/me Endpunkt abzurufen:

var userInfo = new AsyncLazy<UserInformation>(() => GetUserInformationAsync());

public async Task<UserInformation> GetUserInformationAsync() 
{
    // Get the token for the current user
    var authInfo = await GetTokenAsync();

    // Construct the request
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri(client.Endpoint, "/.auth/me"));
    request.Headers.Add("X-ZUMO-AUTH", authInfo.Token);

    // Create a new HttpClient, then send the request
    var httpClient = new HttpClient();
    var response = await httpClient.SendAsync(request);

    // If the request is successful, deserialize the content into the UserInformation object.
    // You will have to create the UserInformation class.
    if (response.IsSuccessStatusCode) 
    {
        var content = await response.ReadAsStringAsync();
        return JsonSerializer.Deserialize<UserInformation>(content);
    }
}

Weiterführende Themen

Löschen von Entitäten in der lokalen Datenbank

Unter normalem Vorgang ist das Löschen von Entitäten nicht erforderlich. Durch den Synchronisierungsprozess werden gelöschte Entitäten entfernt und Standard die erforderlichen Metadaten für lokale Datenbanktabellen enthalten. Es gibt jedoch Situationen, in denen das Löschen von Entitäten innerhalb der Datenbank hilfreich ist. Ein solches Szenario ist, wenn Sie eine große Anzahl von Entitäten löschen müssen, und es ist effizienter, Daten aus der Tabelle lokal zu löschen.

Verwenden Sie table.PurgeItemsAsync()Folgendes, um Datensätze aus einer Tabelle zu löschen:

var query = table.CreateQuery();
var purgeOptions = new PurgeOptions();
await table.PurgeItermsAsync(query, purgeOptions, cancellationToken);

Die Abfrage identifiziert die Entitäten, die aus der Tabelle entfernt werden sollen. Identifizieren Sie die Entitäten, die mithilfe von LINQ gelöscht werden sollen:

var query = table.CreateQuery().Where(m => m.Archived == true);

Die PurgeOptions Klasse stellt Einstellungen zum Ändern des Bereinigungsvorgangs bereit:

  • DiscardPendingOperationsdis Karte alle ausstehenden Vorgänge für die Tabelle, die sich in der Betriebswarteschlange befinden, die auf das Senden an den Server wartet.
  • QueryId Gibt eine Abfrage-ID an, die verwendet wird, um das delta-Token zu identifizieren, das für den Vorgang verwendet werden soll.
  • TimestampUpdatePolicy Gibt an, wie das Delta-Token am Ende des Bereinigungsvorgangs angepasst wird:
    • TimestampUpdatePolicy.NoUpdate gibt an, dass das Delta-Token nicht aktualisiert werden darf.
    • TimestampUpdatePolicy.UpdateToLastEntity gibt an, dass das Delta-Token auf das updatedAt Feld für die letzte Entität aktualisiert werden soll, die in der Tabelle gespeichert ist.
    • TimestampUpdatePolicy.UpdateToNow gibt an, dass das Delta-Token auf das aktuelle Datum/die aktuelle Uhrzeit aktualisiert werden soll.
    • TimestampUpdatePolicy.UpdateToEpoch gibt an, dass das Delta-Token zurückgesetzt werden soll, um alle Daten zu synchronisieren.

Verwenden Sie denselben QueryId Wert, den Sie beim Aufrufen table.PullItemsAsync() zum Synchronisieren von Daten verwendet haben. Das QueryId Specifies the delta token to update when the purge is complete.

Anpassen von Anforderungsheadern

Um Ihr spezielles App-Szenario zu unterstützen, müssen Sie unter Umständen die Kommunikation mit dem Mobile App-Back-End anpassen. Sie können beispielsweise jeder ausgehenden Anforderung einen benutzerdefinierten Header hinzufügen oder Antwortstatuscodes ändern, bevor Sie an den Benutzer zurückkehren. Verwenden Sie einen benutzerdefinierten DelegatingHandler, wie im folgenden Beispiel gezeigt:

public async Task CallClientWithHandler()
{
    var options = new DatasyncClientOptions
    {
        HttpPipeline = new DelegatingHandler[] { new MyHandler() }
    };
    var client = new Datasync("AppUrl", options);
    var todoTable = client.GetRemoteTable<TodoItem>();
    var newItem = new TodoItem { Text = "Hello world", Complete = false };
    await todoTable.InsertItemAsync(newItem);
}

public class MyHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Change the request-side here based on the HttpRequestMessage
        request.Headers.Add("x-my-header", "my value");

        // Do the request
        var response = await base.SendAsync(request, cancellationToken);

        // Change the response-side here based on the HttpResponseMessage

        // Return the modified response
        return response;
    }
}

Aktivieren der Anforderungsprotokollierung

Sie können auch einen DelegatingHandler verwenden, um die Anforderungsprotokollierung hinzuzufügen:

public class LoggingHandler : DelegatingHandler
{
    public LoggingHandler() : base() { }
    public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}");
        if (request.Content != null)
        {
            Debug.WriteLine($"[HTTP] >>> {await request.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        HttpResponseMessage response = await base.SendAsync(request, token).ConfigureAwait(false);

        Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}");
        if (response.Content != null)
        {
            Debug.WriteLine($"[HTTP] <<< {await response.Content.ReadAsStringAsync().ConfigureAwait(false)}");
        }

        return response;
    }
}

Überwachen von Synchronisierungsereignissen

Wenn ein Synchronisierungsereignis auftritt, wird das Ereignis an den client.SynchronizationProgress Ereignisdelegat veröffentlicht. Die Ereignisse können verwendet werden, um den Fortschritt des Synchronisierungsprozesses zu überwachen. Definieren Sie einen Synchronisierungsereignishandler wie folgt:

client.SynchronizationProgress += (sender, args) => {
    // args is of type SynchronizationEventArgs
};

Der SynchronizationEventArgs-Typ ist wie folgt definiert:

public enum SynchronizationEventType
{
    PushStarted,
    ItemWillBePushed,
    ItemWasPushed,
    PushFinished,
    PullStarted,
    ItemWillBeStored,
    ItemWasStored,
    PullFinished
}

public class SynchronizationEventArgs
{
    public SynchronizationEventType EventType { get; }
    public string ItemId { get; }
    public long ItemsProcessed { get; } 
    public long QueueLength { get; }
    public string TableName { get; }
    public bool IsSuccessful { get; }
}

Die Darin enthaltenen args Eigenschaften sind entweder null oder -1 wenn die Eigenschaft für das Synchronisierungsereignis nicht relevant ist.