Jak używać biblioteki klienta usługi Azure Mobile Apps dla platformy .NET
Nuta
Ten produkt jest wycofany. Aby zastąpić projekty przy użyciu platformy .NET 8 lub nowszej, zobacz bibliotekę datasync zestawu narzędzi Community Toolkit.
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:
-
AvaloniaUI -
MAUI (Android i iOS) - uno Platform
- windows (UWP)
- systemu Windows (WinUI3)
- windows (WPF)
- platformy Xamarin (natywna wersja systemu Android)
- Xamarin (natywna wersja systemu iOS)
-
Xamarin Forms (Android i iOS)
Nuta
W tym artykule opisano najnowszą (6.0) edycję platformy Microsoft Datasync Framework. W przypadku starszych klientów zobacz dokumentację w 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 utworzono przykłady dla Avalonia i Uno Platform. Przykładowy todoApp
Konfiguracja i wymagania wstępne
Dodaj następujące biblioteki z narzędzia NuGet:
- Microsoft.Datasync.Client
- Microsoft.Datasync.Client.SQLiteStore w przypadku korzystania z tabel w trybie offline.
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
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 nagłówek Authorization
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ą dostępne dla rejestrowania
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()}"
}
Identyfikator instalacji
Jeśli ustawiono InstallationId
, zostanie wysłany niestandardowy nagłówek X-ZUMO-INSTALLATION-ID
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 InstallationId
, identyfikator powinien być przechowywany w magazynie trwałym na urządzeniu, aby można było śledzić unikatowe instalacje.
Magazyn offline
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.
SerializerSettings
Jeśli zmieniono ustawienia serializatora na serwerze synchronizacji danych, należy wprowadzić te same zmiany w SerializerSettings
na kliencie. Ta opcja umożliwia określenie własnych ustawień serializatora.
TableEndpointResolver
Zgodnie z konwencją tabele znajdują się w usłudze zdalnej w ścieżce /tables/{tableName}
(zgodnie z atrybutem Route
w kodzie serwera). Jednak tabele mogą istnieć w dowolnej ścieżce punktu końcowego.
TableEndpointResolver
jest funkcją, która zamienia nazwę tabeli w ścieżkę do komunikacji z usługą zdalną.
Na przykład następujące zmiany zakładają, że wszystkie tabele znajdują się w /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ść UserAgent
można ustawić na dowolną prawidłową 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ówiono następujące tematy:
- Tworzenie odwołania do tabeli
- Wykonywanie zapytań dotyczących danych
- Liczba elementów z zapytania
- wyszukiwanie danych zdalnych według identyfikatora
- Wstawianie danych na serwerze zdalnym
- Aktualizowanie danych na serwerze zdalnym
- Usuwanie danych na serwerze zdalnym
- rozwiązywanie konfliktów i optymistyczne współbieżności
Tworzenie odwołania do tabeli zdalnej
Aby utworzyć odwołanie do tabeli zdalnej, użyj GetRemoteTable<T>
:
IRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();
Jeśli chcesz zwrócić tabelę tylko do odczytu, użyj wersji IReadOnlyRemoteTable<T>
:
IReadOnlyRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();
Typ modelu musi implementować kontrakt ITableData
z usługi. Użyj DatasyncClientData
, aby podać wymagane pola:
public class TodoItem : DatasyncClientData
{
public string Title { get; set; }
public bool IsComplete { get; set; }
}
Obiekt DatasyncClientData
obejmuje:
-
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ślitrue
, element 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 klauzul
.OrderBy()
. - Wybieranie właściwości za pomocą
.Select()
. - Stronicowanie przy użyciu
.Skip()
i.Take()
.
Zlicz elementy z zapytania
Jeśli potrzebujesz liczby elementów zwracanych przez zapytanie, możesz użyć .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 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>
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")
).
Struktura synchronizacji danych udostępnia również ConcurrentObservableCollection<T>
— 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ć ConcurrentObservableCollection<T>
bezpośrednio z tabeli lub zapytania:
var collection = new ConcurrentObservableCollection<T>();
await remoteTable.ToObservableCollection(collection);
Użycie .ToObservableCollection(collection)
wyzwala zdarzenie CollectionChanged
raz dla całej kolekcji, a nie dla poszczególnych elementów, co powoduje szybsze ponowne rysowanie czasu.
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
Do filtrowania danych można użyć klauzuli .Where()
. 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"));
Pierwsza klauzula .Where()
(zwracanie tylko niekompletnych elementów) jest wykonywana w usłudze, natomiast druga klauzula .Where()
(rozpoczynająca się od "The") jest wykonywana na kliencie.
Klauzula Where
obsługuje operacje, które można przetłumaczyć na podzbiór OData. Operacje obejmują:
- Operatory relacyjne (
==
,!=
,<
,<=
,>
,>=
), - Operatory arytmetyczne (
+
,-
,/
,*
,%
), - Precyzja liczb (
Math.Floor
,Math.Ceiling
), - Funkcje ciągów (
Length
,Substring
,Replace
,IndexOf
,Equals
,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()
, .OrderByDescending()
, .ThenBy()
i .ThenByDescending()
z akcesorem właściwości w celu sortowania danych.
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 .Skip()
i .Take()
w celu zaimplementowania stronicowania:
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 metody InsertItemAsync
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 ReplaceItemAsync
w celu zaktualizowania istniejącego rekordu przy użyciu 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 metody DeleteItemAsync
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 version
zdefiniowanej dla każdej tabeli w zapleczu aplikacji mobilnej. Za każdym razem, gdy rekord jest aktualizowany, usługa Mobile Apps ustawia właściwość version
dla tego rekordu na nową wartość. Podczas każdego żądania aktualizacji właściwość version
rekordu dołączonego do żądania jest porównywana z tą samą właściwością rekordu na serwerze. Jeśli wersja przekazana z żądaniem nie jest zgodna z zapleczem, biblioteka kliencka zgłasza wyjątek DatasyncConflictException<T>
. 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ą wartością version
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ć wyjątek DatasyncConflictException<T>
w kodzie. Rozwiąż konflikt, stosując poprawną version
do zaktualizowanego rekordu, a następnie powtórz wywołanie z rozpoznanymi rekordami. 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 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. OfflineConnectionString 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 nazwach identyfikatorów URI w pliku SQLite.
- Aby użyć pamięci podręcznej w pamięci, użyj
file:inmemory.db?mode=memory&cache=private
. - Aby użyć pliku, użyj
file:/path/to/file.db
Musisz określić bezwzględną nazwę pliku. Jeśli używasz 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 programu 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 metody GetOfflineTable<T>
:
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).
Nuta
Pierwszym argumentem PullItemsAsync
jest zapytanie OData wskazujące rekordy do ściągnięcia na urządzenie. 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 obiekt PullOptions
) nie muszą być zwykle ustawiane. Dostępne są następujące 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 niejawne 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 umieszczane w jednym PushFailedException
. 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 podzbioru tabel, podaj IEnumerable<string>
do metody PushTablesAsync()
:
var tablesToPush = new string[] { "TodoItem", "Notes" };
await client.PushTables(tablesToPush);
Użyj właściwości client.PendingOperations
, aby odczytać liczbę operacji oczekujących na wypchnięcie do usługi zdalnej. Ta właściwość jest null
, 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 metody ExecuteQueryAsync()
. Aby na przykład wykonać instrukcję SQL JOIN
, zdefiniuj JObject
, która pokazuje strukturę wartości zwracanej, a następnie użyj 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 0L
dla liczb (długich), false
dla wartości logicznych i string.Empty
dla wszystkich innych elementów.
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 platformy tożsamości firmy Microsoft
Platforma tożsamości firmy Microsoft umożliwia łatwą integrację z usługą Microsoft Entra ID. 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 integracji platformy tożsamości firmy Microsoft z ASP.NET 6, zobacz dokumentację platformy tożsamości firmy Microsoft
Korzystanie z narzędzi Xamarin Essentials lub MAUI WebAuthenticator
W przypadku uwierzytelniania usługi Azure App Service możesz użyć WebAuthenticator Xamarin Essentials WebAuthenticator 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
};
}
UserId
i DisplayName
nie są dostępne bezpośrednio podczas korzystania z uwierzytelniania usługi Azure App Service. 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 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 modyfikowania 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 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 polaupdatedAt
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 w celu zsynchronizowania wszystkich danych.
-
Użyj tej samej wartości QueryId
, która była używana podczas wywoływania table.PullItemsAsync()
do synchronizowania danych.
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 niestandardowej 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 w delegatu zdarzenia client.SynchronizationProgress
. 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 SynchronizationEventArgs
jest zdefiniowany 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 args
są null
lub -1
, gdy właściwość nie ma znaczenia dla zdarzenia synchronizacji.