Jak używać biblioteki klienta usługi Azure Mobile Apps dla platformy .NET

W tym przewodniku przedstawiono sposób wykonywania typowych scenariuszy przy użyciu biblioteki klienta platformy .NET dla usługi Azure Mobile Apps. Użyj biblioteki klienta .NET w dowolnej aplikacji .NET 6 lub .NET Standard 2.0, w tym MAUI, Xamarin i Windows (WPF, UWP i WinUI).

Jeśli dopiero zaczynasz korzystać z usługi Azure Mobile Apps, rozważ najpierw ukończenie jednego z samouczków szybki start:

Uwaga

W tym artykule opisano najnowszą (6.0) edycję platformy Microsoft Datasync Framework. W przypadku starszych klientów zapoznaj się z dokumentacją wersji 4.2.0.

Obsługiwane platformy

Biblioteka klienta platformy .NET obsługuje dowolną platformę .NET Standard 2.0 lub .NET 6, w tym:

  • .NET MAUI dla platform Android, iOS i Windows.
  • Interfejs API systemu Android na poziomie 21 lub nowszym (Xamarin i Android dla platformy .NET).
  • System iOS w wersji 12.0 lub nowszej (Xamarin i iOS dla platformy .NET).
  • platforma uniwersalna systemu Windows kompiluje 19041 i nowsze wersje.
  • Windows Presentation Framework (WPF).
  • Zestaw SDK aplikacji systemu Windows (WinUI 3).
  • Xamarin.Forms

Ponadto próbki zostały utworzone dla Avalonia i Uno Platform. Przykład aplikacji TodoApp zawiera przykład każdej przetestowanej platformy.

Konfiguracja i wymagania wstępne

Dodaj następujące biblioteki z narzędzia NuGet:

W przypadku korzystania z projektu platformy (na przykład .NET MAUI) upewnij się, że do projektu platformy i dowolnego udostępnionego projektu dodano biblioteki.

Tworzenie klienta usługi

Poniższy kod tworzy klienta usługi, który służy do koordynowania całej komunikacji z tabelami zaplecza i offline.

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

W poprzednim kodzie zastąp MOBILE_APP_URL ciąg adresem URL zaplecza ASP.NET Core. Klient powinien zostać utworzony jako pojedynczy. Jeśli używasz dostawcy uwierzytelniania, można go skonfigurować w następujący sposób:

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

Więcej informacji na temat dostawcy uwierzytelniania podano w dalszej części tego dokumentu.

Opcje

Pełny (domyślny) zestaw opcji można utworzyć w następujący sposób:

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

Zwykle żądanie HTTP jest wykonywane przez przekazanie żądania za pośrednictwem dostawcy uwierzytelniania (który dodaje Authorization nagłówek dla aktualnie uwierzytelnionego użytkownika) przed wysłaniem żądania. Opcjonalnie możesz dodać więcej procedur obsługi delegowania. Każde żądanie przechodzi przez procedury obsługi delegowania przed wysłaniem do usługi. Delegowanie procedur obsługi umożliwia dodawanie dodatkowych nagłówków, ponawianie prób lub udostępnianie możliwości rejestrowania.

Przykłady delegowania procedur obsługi są udostępniane do rejestrowania i dodawania nagłówków żądań w dalszej części tego artykułu.

IdGenerator

Po dodaniu jednostki do tabeli offline musi mieć identyfikator. Identyfikator jest generowany, jeśli go nie podano. Opcja IdGenerator umożliwia dostosowanie wygenerowanego identyfikatora. Domyślnie generowany jest globalnie unikatowy identyfikator. Na przykład następujące ustawienie generuje ciąg zawierający nazwę tabeli i identyfikator GUID:

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

Installationid

Jeśli element InstallationId jest ustawiony, nagłówek X-ZUMO-INSTALLATION-ID niestandardowy jest wysyłany z każdym żądaniem w celu zidentyfikowania kombinacji aplikacji na określonym urządzeniu. Ten nagłówek można rejestrować w dziennikach i umożliwia określenie liczby odrębnych instalacji aplikacji. Jeśli używasz InstallationIdmetody , identyfikator powinien być przechowywany w magazynie trwałym na urządzeniu, aby można było śledzić unikatowe instalacje.

Magazyn offline

Element OfflineStore jest używany podczas konfigurowania dostępu do danych w trybie offline. Aby uzyskać więcej informacji, zobacz Praca z tabelami offline.

ParallelOperations

Część procesu synchronizacji offline obejmuje wypychanie operacji w kolejce do serwera zdalnego. Po wyzwoleniu operacji wypychania operacje są przesyłane w kolejności, w której zostały odebrane. Opcjonalnie możesz użyć maksymalnie ośmiu wątków, aby wypchnąć te operacje. Operacje równoległe wykorzystują więcej zasobów zarówno na kliencie, jak i serwerze, aby szybciej ukończyć operację. Kolejność, w jakiej operacje docierają do serwera, nie może być gwarantowana w przypadku korzystania z wielu wątków.

Serializator Ustawienia

Jeśli zmieniono ustawienia serializatora na serwerze synchronizacji danych, należy wprowadzić te same zmiany na SerializerSettings kliencie. Ta opcja umożliwia określenie własnych ustawień serializatora.

TableEndpointResolver

Zgodnie z konwencją tabele znajdują się w usłudze zdalnej w /tables/{tableName} ścieżce (zgodnie z atrybutem Route w kodzie serwera). Jednak tabele mogą istnieć w dowolnej ścieżce punktu końcowego. Jest TableEndpointResolver to funkcja, która zamienia nazwę tabeli w ścieżkę komunikacji z usługą zdalną.

Na przykład następujące zmiany zakładają, że wszystkie tabele znajdują się w obszarze /api:

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

UserAgent

Klient synchronizacji danych generuje odpowiednią wartość nagłówka User-Agent na podstawie wersji biblioteki. Niektórzy deweloperzy uważają, że nagłówek agenta użytkownika przecieka informacje o kliencie. Właściwość można ustawić na dowolną prawidłową UserAgent wartość nagłówka.

Praca z tabelami zdalnymi

W poniższej sekcji opisano sposób wyszukiwania i pobierania rekordów oraz modyfikowania danych w tabeli zdalnej. Omówione są następujące tematy:

Tworzenie odwołania do tabeli zdalnej

Aby utworzyć odwołanie do tabeli zdalnej, użyj polecenia GetRemoteTable<T>:

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

Jeśli chcesz zwrócić tabelę tylko do odczytu, użyj IReadOnlyRemoteTable<T> wersji:

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

Typ modelu musi implementować ITableData kontrakt z usługi. Użyj DatasyncClientData polecenia , aby podać wymagane pola:

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

Obiekt DatasyncClientData zawiera:

  • Id (ciąg) — globalnie unikatowy identyfikator elementu.
  • UpdatedAt (System.DataTimeOffset) — data/godzina ostatniej aktualizacji elementu.
  • Version (ciąg) — nieprzezroczystym ciągiem używanym do przechowywania wersji.
  • Deleted (wartość logiczna) — jeśli trueelement zostanie usunięty.

Usługa obsługuje te pola. Nie dopasuj tych pól w ramach aplikacji klienckiej.

Modele można dodawać adnotacje przy użyciu atrybutów Newtonsoft.JSON. Nazwę tabeli można określić przy użyciu atrybutu DataTable :

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

Alternatywnie określ nazwę tabeli w wywołaniu GetRemoteTable() :

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

Klient używa ścieżki /tables/{tablename} jako identyfikatora URI. Nazwa tabeli jest również nazwą tabeli offline w bazie danych SQLite.

Obsługiwane typy

Oprócz typów pierwotnych (int, float, string itp.) obsługiwane są następujące typy dla modeli:

  • System.DateTime - jako ciąg daty/godziny ISO-8601 UTC z dokładnością ms.
  • System.DateTimeOffset - jako ciąg daty/godziny ISO-8601 UTC z dokładnością ms.
  • System.Guid - sformatowany jako 32 cyfry oddzielone łącznikami.

Wykonywanie zapytań dotyczących danych z serwera zdalnego

Tabelę zdalną można używać z instrukcjami przypominającymi LINQ, w tym:

  • Filtrowanie za pomocą klauzuli .Where() .
  • Sortowanie przy użyciu różnych .OrderBy() klauzul.
  • Wybieranie właściwości za pomocą polecenia .Select().
  • Stronicowanie za pomocą .Skip() i .Take().

Zlicz elementy z zapytania

Jeśli potrzebujesz liczby elementów zwracanych przez zapytanie, możesz użyć jej .CountItemsAsync() w tabeli lub .LongCountAsync() w zapytaniu:

// 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();

Ta metoda powoduje zaokrąglenie do serwera. Liczbę można również uzyskać podczas wypełniania listy (na przykład), unikając dodatkowej rundy:

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);
}

Liczba zostanie wypełniona po pierwszym żądaniu pobrania zawartości tabeli.

Zwracanie wszystkich danych

Dane są zwracane za pośrednictwem elementu IAsyncEnumerable:

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

Użyj dowolnej z następujących klauzul zakończenia, aby przekonwertować IAsyncEnumerable<T> element na inną kolekcję:

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();

W tle tabela zdalna obsługuje stronicowanie wyniku. Wszystkie elementy są zwracane niezależnie od liczby żądań po stronie serwera wymaganych do spełnienia zapytania. Te elementy są również dostępne w wynikach zapytania (na przykład remoteTable.Where(m => m.Rating == "R")).

Platforma synchronizacji danych zapewnia ConcurrentObservableCollection<T> również bezpieczną wątkowo kolekcję. Tej klasy można używać w kontekście aplikacji interfejsu użytkownika, które zwykle używają ObservableCollection<T> do zarządzania listą (na przykład list Xamarin Forms lub MAUI). Możesz wyczyścić i załadować element ConcurrentObservableCollection<T> bezpośrednio z tabeli lub zapytania:

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

Użycie .ToObservableCollection(collection) wyzwala CollectionChanged zdarzenia raz dla całej kolekcji, a nie dla poszczególnych elementów, co powoduje szybsze ponowne rysowanie czasu.

Element ConcurrentObservableCollection<T> zawiera również modyfikacje oparte na predykacie:

// 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);

Modyfikacje oparte na predykacie mogą być używane w programach obsługi zdarzeń, gdy indeks elementu nie jest znany z wyprzedzeniem.

Filtrowanie danych

Klauzulę można użyć do filtrowania .Where() danych. Na przykład:

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

Filtrowanie odbywa się w usłudze przed elementem IAsyncEnumerable i na kliencie po dokonaniu operacji IAsyncEnumerable. Na przykład:

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

.Where() Pierwsza klauzula (zwraca tylko niekompletne elementy) jest wykonywana w usłudze, natomiast druga .Where() klauzula (zaczynająca się od "The") jest wykonywana na kliencie.

Klauzula Where obsługuje operacje, które można przetłumaczyć na podzestaw OData. Operacje obejmują:

  • Operatory relacyjne (==, !=, <, <=, >, >=),
  • Operatory arytmetyczne (+, -, /, *, ), %
  • Precyzja liczb (Math.Floor, Math.Ceiling),
  • Funkcje ciągów (Length, , Substring, EqualsReplaceIndexOf, , StartsWith, EndsWith) (tylko kultury porządkowe i niezmienne),
  • Właściwości daty (Year, Month, Day, Hour, Minute, Second),
  • Uzyskiwanie dostępu do właściwości obiektu i
  • Wyrażenia łączące dowolną z tych operacji.

Sortowanie danych

Użyj .OrderBy()metody , .OrderByDescending(), .ThenBy()i .ThenByDescending() z akcesorem właściwości, aby sortować dane.

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

Sortowanie odbywa się przez usługę. Nie można określić wyrażenia w żadnej klauzuli sortowania. Jeśli chcesz sortować według wyrażenia, użyj sortowania po stronie klienta:

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

Wybieranie właściwości

Możesz zwrócić podzbiór danych z usługi:

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

Zwracanie strony danych

Możesz zwrócić podzbiór zestawu danych przy użyciu funkcji .Skip() i .Take() zaimplementować stronicowanie:

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

W rzeczywistej aplikacji można użyć zapytań podobnych do poprzedniego przykładu z kontrolką pager lub porównywalnym interfejsem użytkownika, aby nawigować między stronami.

Wszystkie opisane do tej pory funkcje są addytywne, więc możemy zachować ich łańcuch. Każde wywołanie łańcuchowe ma wpływ na więcej zapytania. Jeszcze jeden przykład:

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

Wyszukiwanie danych zdalnych według identyfikatora

Funkcja GetItemAsync może służyć do wyszukiwania obiektów z bazy danych o określonym identyfikatorze.

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

Jeśli element, który próbujesz pobrać, został usunięty nietrwale, należy użyć parametru includeDeleted :

// 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);

Wstawianie danych na serwerze zdalnym

Wszystkie typy klientów muszą zawierać element członkowski o nazwie Id, który jest domyślnie ciągiem. Ten identyfikator jest wymagany do wykonywania operacji CRUD i synchronizacji w trybie offline. Poniższy kod ilustruje sposób użycia InsertItemAsync metody w celu wstawienia nowych wierszy do tabeli. Parametr zawiera dane do wstawienia jako obiekt .NET.

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

Jeśli unikatowa wartość identyfikatora niestandardowego nie jest uwzględniona w item podczas wstawiania, serwer generuje identyfikator. Wygenerowany identyfikator można pobrać, sprawdzając obiekt po powrocie wywołania.

Aktualizowanie danych na serwerze zdalnym

Poniższy kod ilustruje sposób użycia metody w celu zaktualizowania istniejącego rekordu przy użyciu ReplaceItemAsync tego samego identyfikatora przy użyciu nowych informacji.

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

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

Usuwanie danych na serwerze zdalnym

Poniższy kod ilustruje sposób użycia DeleteItemAsync metody do usunięcia istniejącego wystąpienia.

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

await todoTable.DeleteItemAsync(item);

Rozwiązywanie konfliktów i optymistyczna współbieżność

Co najmniej dwóch klientów może zapisywać zmiany w tym samym elemencie w tym samym czasie. Bez wykrywania konfliktów ostatni zapis zastąpi wszystkie poprzednie aktualizacje. Optymistyczna kontrola współbieżności zakłada, że każda transakcja może zatwierdzić i dlatego nie używa żadnych blokad zasobów. Optymistyczna kontrola współbieżności sprawdza, czy żadna inna transakcja nie zmodyfikowała danych przed zatwierdzeniem danych. Jeśli dane zostały zmodyfikowane, transakcja zostanie wycofana.

Usługa Azure Mobile Apps obsługuje optymistyczną kontrolę współbieżności, śledząc zmiany w każdym elemencie przy użyciu kolumny właściwości systemu zdefiniowanej version dla każdej tabeli w zapleczu aplikacji mobilnej. Za każdym razem, gdy rekord jest aktualizowany, usługa Mobile Apps ustawia właściwość dla tego rekordu version na nową wartość. Podczas każdego żądania version aktualizacji właściwość rekordu dołączonego do żądania jest porównywana z tą samą właściwością dla rekordu na serwerze. Jeśli wersja przekazana z żądaniem nie jest zgodna z zapleczem DatasyncConflictException<T> , biblioteka klienta zgłasza wyjątek. Typ dołączony do wyjątku to rekord z zaplecza zawierającego wersję serwera rekordu. Następnie aplikacja może użyć tych informacji, aby zdecydować, czy ponownie wykonać żądanie aktualizacji z poprawną version wartością z zaplecza w celu zatwierdzenia zmian.

Optymistyczna współbieżność jest automatycznie włączana podczas korzystania z obiektu podstawowego DatasyncClientData .

Oprócz włączenia optymistycznej współbieżności należy również przechwycić DatasyncConflictException<T> wyjątek w kodzie. Rozwiąż konflikt, stosując poprawność version do zaktualizowanego rekordu, a następnie powtórz wywołanie z rozpoznaną rekordem. Poniższy kod pokazuje, jak rozwiązać konflikt zapisu po wykryciu:

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();
}

Praca z tabelami offline

Tabele offline używają lokalnego magazynu SQLite do przechowywania danych do użycia w trybie offline. Wszystkie operacje tabel są wykonywane względem lokalnego magazynu SQLite zamiast magazynu serwera zdalnego. Upewnij się, że dodano element Microsoft.Datasync.Client.SQLiteStore do każdego projektu platformy i do wszystkich udostępnionych projektów.

Przed utworzeniem odwołania do tabeli należy przygotować magazyn lokalny:

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

Po zdefiniowaniu magazynu można utworzyć klienta:

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

Na koniec należy upewnić się, że możliwości trybu offline zostały zainicjowane:

await client.InitializeOfflineStoreAsync();

Inicjowanie magazynu jest zwykle wykonywane natychmiast po utworzeniu klienta. Offline Połączenie ionString to identyfikator URI służący do określania zarówno lokalizacji bazy danych SQLite, jak i opcji używanych do otwierania bazy danych. Aby uzyskać więcej informacji, zobacz URI Filenames in SQLite (Nazwy plików identyfikatorów URI w sqlite).

  • Aby użyć pamięci podręcznej w pamięci, użyj polecenia file:inmemory.db?mode=memory&cache=private.
  • Aby użyć pliku, użyj polecenia file:/path/to/file.db

Musisz określić bezwzględną nazwę pliku. Jeśli korzystasz z platformy Xamarin, możesz użyć pomocników systemu plików Xamarin Essentials, aby utworzyć ścieżkę: Na przykład:

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

Jeśli używasz narzędzia MAUI, możesz użyć pomocników systemu plików MAUI do utworzenia ścieżki: Na przykład:

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

Tworzenie tabeli w trybie offline

Odwołanie do tabeli można uzyskać przy użyciu GetOfflineTable<T> metody :

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

Podobnie jak w przypadku tabeli zdalnej, można również uwidocznić tabelę offline tylko do odczytu:

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

Nie musisz uwierzytelniać się, aby użyć tabeli trybu offline. Musisz uwierzytelnić się tylko podczas komunikacji z usługą zaplecza.

Synchronizowanie tabeli offline

Tabele offline nie są domyślnie synchronizowane z zapleczem. Synchronizacja jest podzielona na dwie części. Zmiany można wypychać oddzielnie od pobierania nowych elementów. Na przykład:

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"]);
        }
    }
}

Domyślnie wszystkie tabele używają synchronizacji przyrostowej — pobierane są tylko nowe rekordy. Rekord jest uwzględniany dla każdego unikatowego zapytania (generowanego przez utworzenie skrótu MD5 zapytania OData).

Uwaga

Pierwszym argumentem PullItemsAsync jest zapytanie OData wskazujące, które rekordy mają być ściągane do urządzenia. Lepiej zmodyfikować usługę tak, aby zwracała tylko rekordy specyficzne dla użytkownika, a nie tworzyć złożonych zapytań po stronie klienta.

Opcje (zdefiniowane przez PullOptions obiekt) nie muszą być zwykle ustawiane. Dostępne opcje:

  • PushOtherTables — jeśli ustawiono wartość true, wszystkie tabele są wypychane.
  • QueryId — określony identyfikator zapytania do użycia, a nie wygenerowany.
  • WriteDeltaTokenInterval — jak często zapisywać token różnicowy używany do śledzenia synchronizacji przyrostowej.

Zestaw SDK wykonuje niejawną operację PushAsync() przed ściąganiem rekordów.

Obsługa konfliktów odbywa się w metodzie PullAsync() . Obsługa konfliktów w taki sam sposób jak tabele online. Konflikt jest generowany, gdy PullAsync() jest wywoływany zamiast podczas wstawiania, aktualizowania lub usuwania. Jeśli wystąpi wiele konfliktów, są one powiązane z jednym PushFailedExceptionelementem . Obsłuż każdą awarię oddzielnie.

Wypychanie zmian dla wszystkich tabel

Aby wypchnąć wszystkie zmiany do serwera zdalnego, użyj:

await client.PushTablesAsync();

Aby wypchnąć zmiany dla podzestawu tabel, podaj metodę IEnumerable<string>PushTablesAsync() :

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

client.PendingOperations Użyj właściwości , aby odczytać liczbę operacji oczekujących na wypchnięcie do usługi zdalnej. Ta właściwość jest null wtedy, gdy nie skonfigurowano magazynu offline.

Uruchamianie złożonych zapytań SQLite

Jeśli musisz wykonać złożone zapytania SQL względem bazy danych w trybie offline, możesz to zrobić przy użyciu ExecuteQueryAsync() metody . Aby na przykład wykonać instrukcję, zdefiniuj SQL JOIN element JObject , który pokazuje strukturę wartości zwracanej, a następnie użyj polecenia 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.

Definicja jest zestawem kluczy/wartości. Klucze muszą być zgodne z nazwami pól zwracanymi przez zapytanie SQL, a wartości muszą być wartością domyślną typu oczekiwanego. Użyj dla 0L liczb (długich), false dla wartości logicznych i string.Empty dla wszystkich innych.

SqLite ma restrykcyjny zestaw obsługiwanych typów. Daty/godziny są przechowywane jako liczba milisekund od epoki, aby umożliwić porównywanie.

Uwierzytelnianie użytkowników

Usługa Azure Mobile Apps umożliwia generowanie dostawcy uwierzytelniania do obsługi wywołań uwierzytelniania. Określ dostawcę uwierzytelniania podczas konstruowania klienta usługi:

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

Za każdym razem, gdy wymagane jest uwierzytelnianie, dostawca uwierzytelniania jest wywoływany w celu uzyskania tokenu. Ogólny dostawca uwierzytelniania może służyć zarówno do uwierzytelniania opartego na nagłówku autoryzacji, jak i uwierzytelniania opartego na usłudze App Service oraz uwierzytelniania opartego na autoryzacji. Użyj następującego modelu:

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 */"
    };
}

Tokeny uwierzytelniania są buforowane w pamięci (nigdy nie są zapisywane na urządzeniu) i odświeżane w razie potrzeby.

Korzystanie z Platforma tożsamości Microsoft

Platforma tożsamości Microsoft umożliwia łatwą integrację z identyfikatorem Entra firmy Microsoft. Zapoznaj się z samouczkami Szybkiego startu, aby zapoznać się z kompletnym samouczkiem dotyczącym implementowania uwierzytelniania entra firmy Microsoft. Poniższy kod przedstawia przykład pobierania tokenu dostępu:

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;
    }
}

Aby uzyskać więcej informacji na temat integrowania Platforma tożsamości Microsoft z ASP.NET 6, zobacz dokumentację Platforma tożsamości Microsoft.

Korzystanie z narzędzi Xamarin Essentials lub MAUI WebAuthenticator

W przypadku uwierzytelniania usługi aplikacja systemu Azure możesz użyć narzędzia WebAuthenticator Xamarin Essentials lub webAuthenticator MAUI, aby uzyskać token:

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
    };
}

Wartości UserId i DisplayName nie są dostępne bezpośrednio podczas korzystania z uwierzytelniania usługi aplikacja systemu Azure. Zamiast tego użyj leniwego obiektu żądającego, aby pobrać informacje z punktu końcowego /.auth/me :

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);
    }
}

Tematy zaawansowane

Przeczyszczanie jednostek w lokalnej bazie danych

W ramach normalnego działania przeczyszczanie jednostek nie jest wymagane. Proces synchronizacji usuwa usunięte jednostki i utrzymuje wymagane metadane dla lokalnych tabel bazy danych. Jednak czasami przydatne jest przeczyszczanie jednostek w bazie danych. Jednym z takich scenariuszy jest usunięcie dużej liczby jednostek i bardziej wydajne jest wyczyszczenie danych z tabeli lokalnie.

Aby przeczyścić rekordy z tabeli, użyj polecenia table.PurgeItemsAsync():

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

Zapytanie identyfikuje jednostki do usunięcia z tabeli. Zidentyfikuj jednostki do przeczyszczania przy użyciu LINQ:

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

Klasa PurgeOptions udostępnia ustawienia umożliwiające modyfikowanie operacji przeczyszczania:

  • DiscardPendingOperations odrzuca wszystkie oczekujące operacje dla tabeli, które znajdują się w kolejce operacji oczekujące na wysłanie na serwer.
  • QueryId Określa identyfikator zapytania, który jest używany do identyfikowania tokenu różnicowego do użycia dla operacji.
  • TimestampUpdatePolicy określa sposób dostosowywania tokenu różnicowego na końcu operacji przeczyszczania:
    • TimestampUpdatePolicy.NoUpdate wskazuje, że token różnicowy nie może zostać zaktualizowany.
    • TimestampUpdatePolicy.UpdateToLastEntity wskazuje, że token różnicowy powinien zostać zaktualizowany do updatedAt pola dla ostatniej jednostki przechowywanej w tabeli.
    • TimestampUpdatePolicy.UpdateToNow wskazuje, że token różnicowy powinien zostać zaktualizowany do bieżącej daty/godziny.
    • TimestampUpdatePolicy.UpdateToEpoch wskazuje, że token różnicowy powinien zostać zresetowany, aby zsynchronizować wszystkie dane.

Użyj tej samej QueryId wartości, która była używana podczas wywoływania table.PullItemsAsync() w celu synchronizacji danych. Parametr QueryId określa token różnicowy do aktualizacji po zakończeniu przeczyszczania.

Dostosowywanie nagłówków żądań

Aby obsługiwać konkretny scenariusz aplikacji, może być konieczne dostosowanie komunikacji z zapleczem aplikacji mobilnej. Na przykład można dodać nagłówek niestandardowy do każdego wychodzącego żądania lub zmienić kody stanu odpowiedzi przed powrotem do użytkownika. Użyj niestandardowego programu DelegatingHandler, jak w poniższym przykładzie:

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;
    }
}

Włączanie rejestrowania żądań

Możesz również użyć programu DelegatingHandler, aby dodać rejestrowanie żądań:

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;
    }
}

Monitorowanie zdarzeń synchronizacji

Po wystąpieniu zdarzenia synchronizacji zdarzenie jest publikowane do delegata client.SynchronizationProgress zdarzenia. Zdarzenia mogą służyć do monitorowania postępu procesu synchronizacji. Zdefiniuj program obsługi zdarzeń synchronizacji w następujący sposób:

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

Typ jest definiowany SynchronizationEventArgs w następujący sposób:

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; }
}

Właściwości w ramach args programu są albo -1null, gdy właściwość nie jest odpowiednia dla zdarzenia synchronizacji.