Come usare la libreria client di App per dispositivi mobili di Azure per .NET

Questa guida illustra come eseguire scenari comuni usando la libreria client .NET per app per dispositivi mobili di Azure. Usare la libreria client .NET in qualsiasi applicazione .NET 6 o .NET Standard 2.0, tra cui MAUI, Xamarin e Windows (WPF, UWP e WinUI).

Se non si ha familiarità con app per dispositivi mobili di Azure, è consigliabile completare prima di tutto una delle esercitazioni introduttive:

Nota

Questo articolo illustra l'edizione più recente (v6.0) di Microsoft Datasync Framework. Per i client meno recenti, vedere la documentazione v4.2.0.

Piattaforme supportate

La libreria client .NET supporta qualsiasi piattaforma .NET Standard 2.0 o .NET 6, tra cui:

  • .NET MAUI per piattaforme Android, iOS e Windows.
  • Livello API Android 21 e versioni successive (Xamarin e Android per .NET).
  • iOS versione 12.0 e successive (Xamarin e iOS per .NET).
  • piattaforma UWP (Universal Windows Platform) build 19041 e successive.
  • Windows Presentation Framework (WPF).
  • SDK per app di Windows (WinUI 3).
  • Xamarin.Forms

Sono stati inoltre creati esempi per Avalonia e Uno Platform. L'esempio TodoApp contiene un esempio di ogni piattaforma testata.

Installazione e prerequisiti

Aggiungere le librerie seguenti da NuGet:

Se si usa un progetto di piattaforma (ad esempio, .NET MAUI), assicurarsi di aggiungere le librerie al progetto della piattaforma e a qualsiasi progetto condiviso.

Creare il client del servizio

Il codice seguente crea il client del servizio, che viene usato per coordinare tutte le comunicazioni con le tabelle back-end e offline.

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

Nel codice precedente sostituire MOBILE_APP_URL con l'URL del back-end ASP.NET Core. Il client deve essere creato come singleton. Se si usa un provider di autenticazione, può essere configurato come segue:

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

Altre informazioni sul provider di autenticazione sono disponibili più avanti in questo documento.

Opzioni

È possibile creare un set completo (predefinito) di opzioni come segue:

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

In genere, una richiesta HTTP viene effettuata passando la richiesta tramite il provider di autenticazione (che aggiunge l'intestazione Authorization per l'utente attualmente autenticato) prima di inviare la richiesta. Facoltativamente, è possibile aggiungere altri gestori di delega. Ogni richiesta passa attraverso i gestori di delega prima di essere inviati al servizio. La delega dei gestori consente di aggiungere intestazioni aggiuntive, eseguire nuovi tentativi o fornire funzionalità di registrazione.

Esempi di delega dei gestori sono disponibili per la registrazione e l'aggiunta di intestazioni di richiesta più avanti in questo articolo.

IdGenerator

Quando un'entità viene aggiunta a una tabella offline, deve avere un ID. Se non ne viene specificato uno, viene generato un ID. L'opzione IdGenerator consente di personalizzare l'ID generato. Per impostazione predefinita, viene generato un ID univoco globale. Ad esempio, l'impostazione seguente genera una stringa che include il nome della tabella e un GUID:

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

InstallationId

InstallationId Se è impostato, viene inviata un'intestazione personalizzata X-ZUMO-INSTALLATION-ID con ogni richiesta per identificare la combinazione dell'applicazione in un dispositivo specifico. Questa intestazione può essere registrata nei log e consente di determinare il numero di installazioni distinte per l'app. Se si usa InstallationId, l'ID deve essere archiviato nell'archiviazione permanente nel dispositivo in modo che sia possibile tenere traccia delle installazioni univoche.

OfflineStore

Viene OfflineStore utilizzato quando si configura l'accesso ai dati offline. Per altre informazioni, vedere Usare tabelle offline.

ParallelOperations

Parte del processo di sincronizzazione offline comporta il push delle operazioni in coda nel server remoto. Quando viene attivata l'operazione push, le operazioni vengono inviate nell'ordine in cui sono state ricevute. Facoltativamente, è possibile usare fino a otto thread per eseguire il push di queste operazioni. Le operazioni parallele usano più risorse sia sul client che sul server per completare l'operazione più velocemente. L'ordine in cui le operazioni arrivano al server non possono essere garantite quando si usano più thread.

Serializzatore Impostazioni

Se sono state modificate le impostazioni del serializzatore nel server di sincronizzazione dati, è necessario apportare le stesse modifiche a nel SerializerSettings client. Questa opzione consente di specificare le impostazioni del serializzatore.

TableEndpointResolver

Per convenzione, le tabelle si trovano nel servizio remoto nel /tables/{tableName} percorso (come specificato dall'attributo Route nel codice del server). Tuttavia, le tabelle possono esistere in qualsiasi percorso dell'endpoint. TableEndpointResolver è una funzione che trasforma un nome di tabella in un percorso per la comunicazione con il servizio remoto.

Ad esempio, il presupposto seguente cambia in modo che tutte le tabelle si trovino in /api:

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

UserAgent

Il client di sincronizzazione dati genera un valore di intestazione User-Agent appropriato in base alla versione della libreria. Alcuni sviluppatori sentono che l'intestazione dell'agente utente perde informazioni sul client. È possibile impostare la UserAgent proprietà su qualsiasi valore di intestazione valido.

Usare tabelle remote

La sezione seguente illustra in dettaglio come cercare e recuperare i record e modificare i dati all'interno di una tabella remota. Vengono trattati i seguenti argomenti:

Creare un riferimento alla tabella remota

Per creare un riferimento alla tabella remota, usare GetRemoteTable<T>:

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

Se si desidera restituire una tabella di sola lettura, usare la IReadOnlyRemoteTable<T> versione seguente:

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

Il tipo di modello deve implementare il ITableData contratto dal servizio. Usare DatasyncClientData per specificare i campi obbligatori:

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

L'oggetto DatasyncClientData include:

  • Id (string) - ID univoco globale per l'elemento.
  • UpdatedAt (System.DataTimeOffset): data/ora dell'ultimo aggiornamento dell'elemento.
  • Version (stringa): stringa opaca usata per il controllo delle versioni.
  • Deleted (booleano): se true, l'elemento viene eliminato.

Il servizio gestisce questi campi. Non modificare questi campi come parte dell'applicazione client.

I modelli possono essere annotati usando gli attributi Newtonsoft.JSON. Il nome della tabella può essere specificato usando l'attributo DataTable :

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

In alternativa, specificare il nome della tabella nella GetRemoteTable() chiamata:

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

Il client usa il percorso /tables/{tablename} come URI. Il nome della tabella è anche il nome della tabella offline nel database SQLite.

Tipi supportati

A parte i tipi primitivi (int, float, string e così via), per i modelli sono supportati i tipi seguenti:

  • System.DateTime - come stringa di data/ora UTC ISO-8601 con accuratezza ms.
  • System.DateTimeOffset - come stringa di data/ora UTC ISO-8601 con accuratezza ms.
  • System.Guid - formattato come 32 cifre separate come trattini.

Eseguire query sui dati da un server remoto

La tabella remota può essere usata con istruzioni simili a LINQ, tra cui:

  • Applicazione di filtri con una .Where() clausola .
  • Ordinamento con varie .OrderBy() clausole.
  • Selezione delle proprietà con .Select().
  • Paging con .Skip() e .Take().

Contare gli elementi da una query

Se è necessario un conteggio degli elementi restituiti dalla query, è possibile usare .CountItemsAsync() in una tabella o .LongCountAsync() in una query:

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

Questo metodo causa un round trip al server. È anche possibile ottenere un conteggio durante il popolamento di un elenco (ad esempio), evitando il round trip aggiuntivo:

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

Il conteggio verrà popolato dopo la prima richiesta per recuperare il contenuto del sommario.

Restituzione di tutti i dati

I dati vengono restituiti tramite IAsyncEnumerable:

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

Utilizzare una delle clausole di terminazione seguenti per convertire l'oggetto IAsyncEnumerable<T> in una raccolta diversa:

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

Dietro le quinte, la tabella remota gestisce automaticamente il paging del risultato. Tutti gli elementi vengono restituiti indipendentemente dal numero di richieste lato server necessarie per soddisfare la query. Questi elementi sono disponibili anche nei risultati della query , ad esempio remoteTable.Where(m => m.Rating == "R").

Il framework di sincronizzazione dati fornisce ConcurrentObservableCollection<T> anche una raccolta osservabile thread-safe. Questa classe può essere usata nel contesto delle applicazioni dell'interfaccia utente che normalmente usano ObservableCollection<T> per gestire un elenco, ad esempio Xamarin Forms o elenchi MAUI. È possibile cancellare e caricare un oggetto ConcurrentObservableCollection<T> direttamente da una tabella o una query:

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

L'uso .ToObservableCollection(collection) di attiva l'evento CollectionChanged una sola volta per l'intera raccolta anziché per i singoli elementi, con conseguente ridisegno più rapido.

L'oggetto ConcurrentObservableCollection<T> include anche modifiche basate su predicato:

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

Le modifiche basate sui predicati possono essere usate nei gestori eventi quando l'indice dell'elemento non è noto in anticipo.

Filtro dei dati

È possibile usare una .Where() clausola per filtrare i dati. Ad esempio:

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

Il filtro viene eseguito sul servizio prima di IAsyncEnumerable e sul client dopo IAsyncEnumerable. Ad esempio:

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

La prima .Where() clausola (restituisce solo elementi incompleti) viene eseguita nel servizio, mentre la seconda .Where() clausola (a partire da "The") viene eseguita nel client.

La clausola Where supporta operazioni che vengono convertite nel subset OData. Alcune operazioni sono:

  • Operatori relazionali (==, !=, <=<, >), >=
  • Operatori aritmetici (+, -, /*, ), %
  • Precisione numerica (Math.Floor, Math.Ceiling),
  • Funzioni stringa (Length, Substring, ReplaceIndexOf, Equals, StartsWithEndsWith) (solo impostazioni cultura ordinali e invarianti),
  • Proprietà di data (Year, Month, DayHour, Minute), Second
  • Proprietà di accesso di un oggetto
  • Espressioni che combinano queste operazioni.

Ordinamento dei dati

Usare .OrderBy(), .OrderByDescending().ThenBy(), e .ThenByDescending() con una funzione di accesso alle proprietà per ordinare i dati.

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

L'ordinamento viene eseguito dal servizio. Non è possibile specificare un'espressione in alcuna clausola di ordinamento. Se si vuole ordinare in base a un'espressione, usare l'ordinamento lato client:

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

Selezione delle proprietà

È possibile restituire un subset di dati dal servizio:

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

Restituire una pagina di dati

È possibile restituire un subset del set di dati usando .Skip() e .Take() per implementare il paging:

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

In un'app reale è possibile usare query simili all'esempio precedente con un controllo pager o un'interfaccia utente paragonabile per passare da una pagina all'altra.

Tutte le funzioni descritte in precedenza sono di tipo additivo ed è quindi possibile usare la concatenazione. Ogni chiamata concatenata incide ulteriormente sulla query. Un altro esempio:

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

Cercare i dati remoti in base all'ID

Per cercare oggetti dal database caratterizzati da un particolare ID, è possibile usare la funzione GetItemAsync .

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

Se l'elemento che si sta tentando di recuperare è stato eliminato soft-delete, è necessario usare il includeDeleted parametro :

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

Inserire dati nel server remoto

Tutti i tipi di client devono contenere un membro denominato Id, che per impostazione predefinita è una stringa. Questo ID è necessario per eseguire operazioni CRUD e per la sincronizzazione offline. Nel codice seguente viene illustrato come utilizzare il InsertItemAsync metodo per inserire nuove righe in una tabella. Il parametro contiene i dati da inserire come oggetto .NET.

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

Se durante un inserimento non è incluso item un valore ID personalizzato univoco, il server genera un ID. È possibile recuperare l'ID generato esaminando l'oggetto dopo la restituzione della chiamata.

Aggiornare i dati nel server remoto

Il codice seguente illustra come usare il ReplaceItemAsync metodo per aggiornare un record esistente con lo stesso ID con nuove informazioni.

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

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

Eliminare i dati nel server remoto

Il codice seguente illustra come usare il DeleteItemAsync metodo per eliminare un'istanza esistente.

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

await todoTable.DeleteItemAsync(item);

Risoluzione dei conflitti e concorrenza ottimistica

Due o più client possono scrivere modifiche allo stesso elemento contemporaneamente. Se non viene rilevato un conflitto, l'ultima scrittura sovrascrive tutti gli aggiornamenti precedenti. Il controllo della concorrenza ottimistica presuppone che ogni transazione possa eseguire il commit e pertanto non usi alcun blocco delle risorse. Il controllo della concorrenza ottimistica verifica che nessun'altra transazione abbia modificato i dati prima di eseguire il commit dei dati. Se i dati sono stati modificati, viene eseguito il rollback della transazione.

App per dispositivi mobili di Azure supporta il controllo della concorrenza ottimistica monitorando le modifiche apportate a ogni elemento usando la colonna delle version proprietà di sistema definita per ogni tabella nel back-end dell'app per dispositivi mobili. Ogni volta che un record viene aggiornato, App per dispositivi mobili imposta la proprietà version per quel record su un nuovo valore. Durante ogni richiesta di aggiornamento, la proprietà version del record inclusa nella richiesta viene confrontata con la stessa proprietà relativa al record sul server. Se la versione passata con la richiesta non corrisponde al back-end, la libreria client genera un'eccezione DatasyncConflictException<T> . Il tipo incluso nell'eccezione corrisponde al record del back-end contenente la versione dei server del record. L'applicazione può quindi usare questa informazione per decidere se eseguire nuovamente la richiesta di aggiornamento con il valore version corretto dal back-end per effettuare il commit delle modifiche.

La concorrenza ottimistica viene abilitata automaticamente quando si usa l'oggetto DatasyncClientData di base.

Oltre ad abilitare la concorrenza ottimistica, è necessario intercettare anche l'eccezione DatasyncConflictException<T> nel codice. Risolvere il conflitto applicando il valore corretto version al record aggiornato e quindi ripetere la chiamata con il record risolto. Il codice seguente illustra come risolvere un conflitto di scrittura, qualora venga rilevato.

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

Usare le tabelle offline

Le tabelle offline usano un archivio SQLite locale per archiviare dati da usare in modalità offline. Tutte le operazioni delle tabelle vengono eseguite nell'archivio SQLite locale anziché nell'archivio sul server remoto. Assicurarsi di aggiungere a Microsoft.Datasync.Client.SQLiteStore ogni progetto di piattaforma e a tutti i progetti condivisi.

Prima di poter creare un riferimento alla tabella è necessario preparare l'archivio locale:

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

Dopo aver definito l'archivio, è possibile creare il client:

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

Infine, è necessario assicurarsi che le funzionalità offline siano inizializzate:

await client.InitializeOfflineStoreAsync();

L'inizializzazione dell'archivio di solito viene effettuata immediatamente dopo aver creato il client. Offline Connessione ionString è un URI usato per specificare sia il percorso del database SQLite che le opzioni usate per aprire il database. Per altre informazioni, vedere Nomi file URI in SQLite.

  • Per usare una cache in memoria, usare file:inmemory.db?mode=memory&cache=private.
  • Per usare un file, usare file:/path/to/file.db

È necessario specificare il nome file assoluto per il file. Se si usa Xamarin, è possibile usare gli helper del file system Xamarin Essentials per costruire un percorso: ad esempio:

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

Se si usa MAUI, è possibile usare gli helper del file system MAUI per costruire un percorso: ad esempio:

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

Creare una tabella offline

È possibile ottenere un riferimento alla tabella usando il metodo GetOfflineTable<T>:

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

Come per la tabella remota, è anche possibile esporre una tabella offline di sola lettura:

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

Non è necessario eseguire l'autenticazione per usare una tabella offline. È sufficiente eseguire l'autenticazione quando si comunica con il servizio back-end.

Sincronizzare una tabella offline

Le tabelle offline non vengono sincronizzate con il back-end per impostazione predefinita. La sincronizzazione è suddivisa in due parti. È possibile inserire le modifiche separatamente rispetto al download di nuovi elementi. Ad esempio:

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

Per impostazione predefinita, tutte le tabelle usano la sincronizzazione incrementale. Vengono recuperati solo i nuovi record. Un record è incluso per ogni query univoca (generata creando un hash MD5 della query OData).

Nota

Il primo argomento di PullItemsAsync è la query OData che indica quali record eseguire il pull nel dispositivo. È preferibile modificare il servizio in modo che restituisca solo record specifici all'utente anziché creare query complesse sul lato client.

Le opzioni (definite dall'oggetto PullOptions ) in genere non devono essere impostate. Le opzioni includono:

  • PushOtherTables - se impostato su true, viene eseguito il push di tutte le tabelle.
  • QueryId : ID di query specifico da usare anziché quello generato.
  • WriteDeltaTokenInterval : frequenza con cui scrivere il token differenziale usato per tenere traccia della sincronizzazione incrementale.

L'SDK esegue un PushAsync() implicito prima di estrarre i record.

La gestione dei conflitti viene eseguita su un metodo PullAsync(). Gestire i conflitti nello stesso modo delle tabelle online. Il conflitto viene generato quando viene chiamato PullAsync() al posto di durante l'inserimento, l'aggiornamento o l'eliminazione. Se si verificano più conflitti, vengono raggruppati in un singolo PushFailedExceptionoggetto . Gestire separatamente ogni errore.

Eseguire il push delle modifiche per tutte le tabelle

Per eseguire il push di tutte le modifiche al server remoto, usare:

await client.PushTablesAsync();

Per eseguire il push delle modifiche per un subset di tabelle, fornire un oggetto IEnumerable<string> al PushTablesAsync() metodo :

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

Utilizzare la client.PendingOperations proprietà per leggere il numero di operazioni in attesa di essere inoltrate al servizio remoto. Questa proprietà è null quando non è stato configurato alcun archivio offline.

Eseguire query SQLite complesse

Se è necessario eseguire query SQL complesse sul database offline, è possibile farlo usando il ExecuteQueryAsync() metodo . Ad esempio, per eseguire un'istruzione SQL JOIN , definire un oggetto JObject che mostra la struttura del valore restituito, quindi usare 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.

La definizione è un set di chiavi/valori. Le chiavi devono corrispondere ai nomi di campo restituiti dalla query SQL e i valori devono essere il valore predefinito del tipo previsto. Usare 0L per i numeri (long), false per i booleani e string.Empty per tutto il resto.

SQLite ha un set restrittivo di tipi supportati. Data/ora vengono archiviate come numero di millisecondi dal momento che il periodo consente confronti.

Autenticare gli utenti

App per dispositivi mobili di Azure consente di generare un provider di autenticazione per la gestione delle chiamate di autenticazione. Specificare il provider di autenticazione quando si costruisce il client del servizio:

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

Ogni volta che è necessaria l'autenticazione, viene chiamato il provider di autenticazione per ottenere il token. Un provider di autenticazione generico può essere usato sia per l'autenticazione basata sull'intestazione di autorizzazione che per l'autenticazione basata su autenticazione e autorizzazione servizio app. Usare il modello seguente:

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

I token di autenticazione vengono memorizzati nella cache (mai scritti nel dispositivo) e aggiornati quando necessario.

Usare Microsoft Identity Platform

Microsoft Identity Platform consente di integrare facilmente con Microsoft Entra ID. Per un'esercitazione completa su come implementare l'autenticazione di Microsoft Entra, vedere le esercitazioni introduttive. Il codice seguente illustra un esempio di recupero del token di accesso:

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

Per altre informazioni sull'integrazione di Microsoft Identity Platform con ASP.NET 6, vedere la documentazione di Microsoft Identity Platform .

Usare Xamarin Essentials o MAUI WebAuthenticator

Per app Azure'autenticazione del servizio, è possibile usare Xamarin Essentials WebAuthenticator o MAUI WebAuthenticator per ottenere un 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
    };
}

e UserIdDisplayName non sono direttamente disponibili quando si usa app Azure'autenticazione del servizio. Usare invece un richiedente differita per recuperare le informazioni dall'endpoint /.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);
    }
}

Argomenti avanzati

Eliminazione di entità nel database locale

Con il normale funzionamento, l'eliminazione delle entità non è necessaria. Il processo di sincronizzazione rimuove le entità eliminate e mantiene i metadati necessari per le tabelle di database locali. Tuttavia, in alcuni casi è utile eliminare le entità all'interno del database. Uno di questi scenari è quando è necessario eliminare un numero elevato di entità ed è più efficiente cancellare i dati dalla tabella in locale.

Per eliminare i record da una tabella, usare table.PurgeItemsAsync():

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

La query identifica le entità da rimuovere dalla tabella. Identificare le entità da eliminare tramite LINQ:

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

La PurgeOptions classe fornisce le impostazioni per modificare l'operazione di eliminazione:

  • DiscardPendingOperations rimuove tutte le operazioni in sospeso per la tabella che si trovano nella coda delle operazioni in attesa di essere inviate al server.
  • QueryId specifica un ID di query utilizzato per identificare il token differenziale da usare per l'operazione.
  • TimestampUpdatePolicy specifica come modificare il token delta alla fine dell'operazione di eliminazione:
    • TimestampUpdatePolicy.NoUpdate indica che il token delta non deve essere aggiornato.
    • TimestampUpdatePolicy.UpdateToLastEntity indica che il token differenziale deve essere aggiornato al updatedAt campo per l'ultima entità archiviata nella tabella.
    • TimestampUpdatePolicy.UpdateToNow indica che il token differenziale deve essere aggiornato alla data/ora corrente.
    • TimestampUpdatePolicy.UpdateToEpoch indica che il token differenziale deve essere reimpostato per sincronizzare tutti i dati.

Usare lo stesso QueryId valore usato durante la chiamata table.PullItemsAsync() per sincronizzare i dati. QueryId Specifica il token delta da aggiornare al termine dell'eliminazione.

Personalizzare le intestazioni di richieste

Per supportare lo scenario specifico dell'app, potrebbe essere necessario personalizzare la comunicazione con il back-end di App per dispositivi mobili. Ad esempio, è possibile aggiungere un'intestazione personalizzata a ogni richiesta in uscita o modificare i codici di stato della risposta prima di tornare all'utente. Usare un delegato personalizzatoHandler, come nell'esempio seguente:

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

Abilitare la registrazione delle richieste

È anche possibile usare un DelegatongHandler per aggiungere la registrazione delle richieste:

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

Monitorare gli eventi di sincronizzazione

Quando si verifica un evento di sincronizzazione, l'evento viene pubblicato nel delegato dell'evento client.SynchronizationProgress . Gli eventi possono essere usati per monitorare lo stato di avanzamento del processo di sincronizzazione. Definire un gestore eventi di sincronizzazione come segue:

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

Il SynchronizationEventArgs tipo è definito come segue:

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

Le proprietà all'interno args di sono null o -1 quando la proprietà non è rilevante per l'evento di sincronizzazione.