Condividi tramite


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

Nota

Questo prodotto viene ritirato. Per una sostituzione dei progetti che usano .NET 8 o versione successiva, vedere la libreria datasync di Community Toolkit.

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:

  • AvaloniaUI
  • MAUI (Android e iOS)
  • piattaforma uno
  • Windows (UWP)
  • Windows (WinUI3)
  • windows (WPF)
  • Xamarin (Android Native)
  • Xamarin (iOS Native)
  • Xamarin Forms (Android e iOS)

Nota

Questo articolo illustra l'edizione più recente (v6.0) di Microsoft Datasync Framework. Per i client meno recenti, vedere la documentazione di 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).
  • Universal Windows Platform build 19041 e versioni successive.
  • Windows Presentation Framework (WPF).
  • Windows App SDK (WinUI 3).
  • Xamarin.Forms

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

Configurazione 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 di 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

Se viene impostata una InstallationId, 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

Il OfflineStore viene usato durante la configurazione dell'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.

SerializerSettings

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

TableEndpointResolver

Per convenzione, le tabelle si trovano nel servizio remoto nel percorso /tables/{tableName} (come specificato dall'attributo Route nel codice del server). Tuttavia, le tabelle possono esistere in qualsiasi percorso dell'endpoint. Il 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 proprietà UserAgent 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. Gli argomenti seguenti sono trattati:

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 versione IReadOnlyRemoteTable<T>:

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

Il tipo di modello deve implementare il contratto ITableData 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 (stringa): 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 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 chiamata GetRemoteTable():

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:

  • Filtro con una clausola .Where().
  • Ordinamento con varie clausole di .OrderBy().
  • 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 un IAsyncEnumerable:

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

Utilizzare una delle clausole di terminazione seguenti per convertire il 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 anche ConcurrentObservableCollection<T>, 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 ConcurrentObservableCollection<T> direttamente da una tabella o una query:

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

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

Il 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 clausola .Where() per filtrare i dati. Per esempio:

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

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

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

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

La clausola Where supporta operazioni convertite nel subset OData. Le operazioni includono:

  • Operatori relazionali (==, !=, <, <=, >, >=),
  • Operatori aritmetici (+, -, /, *, %),
  • Precisione numerica (Math.Floor, Math.Ceiling),
  • Funzioni stringa (Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith) (solo impostazioni cultura ordinali e invarianti),
  • Proprietà di data (Year, Month, Day, Hour, Minute, Second),
  • Accedere alle proprietà di un oggetto e
  • Espressioni che combinano una di 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 cercapersone o un'interfaccia utente paragonabile per spostarsi tra le pagine.

Tutte le funzioni descritte finora sono additivi, in modo da poterle concatenare. Ogni chiamata concatenato influisce maggiormente 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

La funzione GetItemAsync può essere usata per cercare oggetti dal database con un ID specifico.

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

Se l'elemento che si sta tentando di recuperare è stato eliminato temporanea, è necessario usare il parametro 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);

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 metodo InsertItemAsync 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 un valore ID personalizzato univoco non è incluso nel item durante un inserimento, il server genera un ID. È possibile recuperare l'ID generato esaminando l'oggetto dopo la restituzione della chiamata.

Aggiornare i dati nel server remoto

Nel codice seguente viene illustrato come utilizzare il metodo ReplaceItemAsync 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 metodo DeleteItemAsync 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. Senza il rilevamento dei conflitti, l'ultima scrittura sovrascriverà eventuali aggiornamenti precedenti. 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 proprietà di sistema version 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 tale record su un nuovo valore. Durante ogni richiesta di aggiornamento, la proprietà version del record incluso nella richiesta viene confrontata con la stessa proprietà per il record nel 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 è il record del back-end contenente la versione server del record. L'applicazione può quindi usare queste informazioni per decidere se eseguire nuovamente la richiesta di aggiornamento con il valore di version corretto dal back-end per eseguire il commit delle modifiche.

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

Oltre ad abilitare la concorrenza ottimistica, è necessario intercettare anche l'eccezione DatasyncConflictException<T> nel codice. Risolvere il conflitto applicando il version corretto al record aggiornato e quindi ripetere la chiamata con il record risolto. Il codice seguente illustra come risolvere un conflitto di scrittura una volta 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 i dati da usare quando sono offline. Tutte le operazioni di tabella vengono eseguite sull'archivio SQLite locale anziché sull'archivio server remoto. Assicurarsi di aggiungere il Microsoft.Datasync.Client.SQLiteStore a ogni progetto di piattaforma e a tutti i progetti condivisi.

Prima di poter creare un riferimento a una 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 viene in genere eseguita immediatamente dopo la creazione del client. Il OfflineConnectionString è un URI usato per specificare sia il percorso del database SQLite che le opzioni usate per aprire il database. Per altre informazioni, vedere nomi di 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 a una 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 viene suddivisa in due parti. È possibile eseguire il push delle modifiche separatamente dal download di nuovi elementi. Per 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 da 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 eseguire il pull dei record.

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

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 IEnumerable<string> al metodo PushTablesAsync():

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

Utilizzare la proprietà client.PendingOperations per leggere il numero di operazioni in attesa di push nel 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 metodo ExecuteQueryAsync(). Ad esempio, per eseguire un'istruzione SQL JOIN, definire un 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 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 sull'autenticazione basata sul servizio app e l'autenticazione basata sull'autorizzazione. 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 Microsoft Identity Platform.

Usare Xamarin Essentials o MAUI WebAuthenticator

Per l'autenticazione del servizio app di Azure, è possibile usare il WebAuthenticator Xamarin Essentials o il 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
    };
}

Le UserId e le DisplayName non sono disponibili direttamente quando si usa l'autenticazione del servizio app di Azure. 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 classe PurgeOptions fornisce impostazioni per modificare l'operazione di eliminazione:

  • DiscardPendingOperations elimina 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 usato 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 differenziale non deve essere aggiornato.
    • TimestampUpdatePolicy.UpdateToLastEntity indica che il token differenziale deve essere aggiornato al campo updatedAt 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 valore QueryId usato per chiamare table.PullItemsAsync() per sincronizzare i dati. Il QueryId specifica il token delta da aggiornare al termine dell'eliminazione.

Personalizzare le intestazioni delle richieste

Per supportare lo scenario specifico dell'app, potrebbe essere necessario personalizzare la comunicazione con il back-end dell'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 personalizzato DelegatingHandler, 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 tipo di SynchronizationEventArgs è 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 di args sono null o -1 quando la proprietà non è rilevante per l'evento di sincronizzazione.