Partager via


Utilisation de la bibliothèque de client Azure Mobile Apps pour .NET

Note

Ce produit est mis hors service. Pour un remplacement des projets utilisant .NET 8 ou version ultérieure, consultez la bibliothèque Datasync Community Toolkit.

Ce guide vous montre comment effectuer des scénarios courants à l’aide de la bibliothèque cliente .NET pour Azure Mobile Apps. Utilisez la bibliothèque cliente .NET dans n’importe quelle application .NET 6 ou .NET Standard 2.0, notamment MAUI, Xamarin et Windows (WPF, UWP et WinUI).

Si vous débutez avec Azure Mobile Apps, commencez par suivre l’un des didacticiels de démarrage rapide :

Note

Cet article traite de la dernière édition (v6.0) de Microsoft Datasync Framework. Pour les clients plus anciens, consultez la documentation v4.2.0.

Plateformes prises en charge

La bibliothèque cliente .NET prend en charge toute plateforme .NET Standard 2.0 ou .NET 6, notamment :

  • .NET MAUI pour les plateformes Android, iOS et Windows.
  • Niveau d’API Android 21 et versions ultérieures (Xamarin et Android pour .NET).
  • iOS version 12.0 et ultérieure (Xamarin et iOS pour .NET).
  • La plateforme Windows universelle génère 19041 et versions ultérieures.
  • Windows Presentation Framework (WPF).
  • Kit de développement logiciel (SDK) d’application Windows (WinUI 3).
  • Xamarin.Forms

En outre, des exemples ont été créés pour Avalonia et Uno Platform. L’exemple TodoApp contient un exemple de chaque plateforme testée.

Configuration et conditions préalables

Ajoutez les bibliothèques suivantes à partir de NuGet :

Si vous utilisez un projet de plateforme (par exemple, .NET MAUI), veillez à ajouter les bibliothèques au projet de plateforme et à tout projet partagé.

Créer le client de service

Le code suivant crée le client de service, qui est utilisé pour coordonner toutes les communications vers les tables back-end et hors connexion.

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

Dans le code précédent, remplacez MOBILE_APP_URL par l’URL du serveur principal ASP.NET Core. Le client doit être créé en tant que singleton. Si vous utilisez un fournisseur d’authentification, il peut être configuré comme suit :

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

Vous trouverez plus d’informations sur le fournisseur d’authentification plus loin dans ce document.

Options

Un ensemble complet (par défaut) d’options peut être créé comme suit :

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

Normalement, une requête HTTP est effectuée en transmettant la requête via le fournisseur d’authentification (qui ajoute l’en-tête Authorization pour l’utilisateur actuellement authentifié) avant d’envoyer la requête. Vous pouvez éventuellement ajouter d’autres gestionnaires de délégation. Chaque requête passe par les gestionnaires délégués avant d’être envoyée au service. La délégation de gestionnaires vous permet d’ajouter des en-têtes supplémentaires, d’effectuer des nouvelles tentatives ou de fournir des fonctionnalités de journalisation.

Des exemples de délégation de gestionnaires sont fournis pour de journalisation et l’ajout d’en-têtes de requête plus loin dans cet article.

IdGenerator

Lorsqu’une entité est ajoutée à une table hors connexion, elle doit avoir un ID. Un ID est généré si un ID n’est pas fourni. L’option IdGenerator vous permet de personnaliser l’ID généré. Par défaut, un ID global unique est généré. Par exemple, le paramètre suivant génère une chaîne qui inclut le nom de la table et un GUID :

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

Id d’installation

Si un InstallationId est défini, un en-tête personnalisé X-ZUMO-INSTALLATION-ID est envoyé avec chaque requête pour identifier la combinaison de l’application sur un appareil spécifique. Cet en-tête peut être enregistré dans les journaux et vous permet de déterminer le nombre d’installations distinctes pour votre application. Si vous utilisez InstallationId, l’ID doit être stocké dans un stockage persistant sur l’appareil afin que les installations uniques puissent être suivies.

OfflineStore

Le OfflineStore est utilisé lors de la configuration de l’accès aux données hors connexion. Pour plus d’informations, consultez Utiliser des tables hors connexion.

ParallelOperations

Une partie du processus de synchronisation hors connexion implique l’envoi d’opérations en file d’attente au serveur distant. Lorsque l’opération push est déclenchée, les opérations sont envoyées dans l’ordre dans lequel elles ont été reçues. Vous pouvez éventuellement utiliser jusqu’à huit threads pour envoyer (push) ces opérations. Les opérations parallèles utilisent davantage de ressources sur le client et le serveur pour accélérer l’opération. L’ordre dans lequel les opérations arrivent au serveur ne peut pas être garanti lors de l’utilisation de plusieurs threads.

SerializerSettings

Si vous avez modifié les paramètres de sérialiseur sur le serveur de synchronisation des données, vous devez apporter les mêmes modifications au SerializerSettings sur le client. Cette option vous permet de spécifier vos propres paramètres de sérialiseur.

TableEndpointResolver

Par convention, les tables se trouvent sur le service distant au niveau du chemin d’accès /tables/{tableName} (comme spécifié par l’attribut Route dans le code du serveur). Toutefois, les tables peuvent exister sur n’importe quel chemin de point de terminaison. Le TableEndpointResolver est une fonction qui transforme un nom de table en chemin d’accès pour communiquer avec le service distant.

Par exemple, les modifications suivantes modifient l’hypothèse afin que toutes les tables se trouvent sous /api:

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

UserAgent

Le client de synchronisation de données génère une valeur d’en-tête User-Agent appropriée en fonction de la version de la bibliothèque. Certains développeurs estiment que l’en-tête de l’agent utilisateur fuite des informations sur le client. Vous pouvez définir la propriété UserAgent sur n’importe quelle valeur d’en-tête valide.

Utiliser des tables distantes

La section suivante explique comment rechercher et récupérer des enregistrements et modifier les données dans une table distante. Les rubriques suivantes sont abordées :

Créer une référence de table distante

Pour créer une référence de table distante, utilisez GetRemoteTable<T>:

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

Si vous souhaitez retourner une table en lecture seule, utilisez la version IReadOnlyRemoteTable<T> :

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

Le type de modèle doit implémenter le contrat ITableData à partir du service. Utilisez DatasyncClientData pour fournir les champs requis :

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

L’objet DatasyncClientData inclut :

  • Id (chaîne) : ID global unique de l’élément.
  • UpdatedAt (System.DataTimeOffset) : date/heure de la dernière mise à jour de l’élément.
  • Version (chaîne) : chaîne opaque utilisée pour le contrôle de version.
  • Deleted (booléen) : si true, l’élément est supprimé.

Le service gère ces champs. N’ajustez pas ces champs dans le cadre de votre application cliente.

Les modèles peuvent être annotés à l’aide d’attributs Newtonsoft.JSON. Le nom de la table peut être spécifié à l’aide de l’attribut DataTable :

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

Vous pouvez également spécifier le nom de la table dans l’appel GetRemoteTable() :

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

Le client utilise le chemin d’accès /tables/{tablename} comme URI. Le nom de la table est également le nom de la table hors connexion dans la base de données SQLite.

Types pris en charge

Outre les types primitifs (int, float, string, etc.), les types suivants sont pris en charge pour les modèles :

  • System.DateTime - en tant que chaîne de date/heure UTC ISO-8601 avec précision ms.
  • System.DateTimeOffset - en tant que chaîne de date/heure UTC ISO-8601 avec précision ms.
  • System.Guid - mis en forme sous la forme de 32 chiffres séparés en traits d’union.

Interroger des données à partir d’un serveur distant

La table distante peut être utilisée avec des instructions de type LINQ, notamment :

  • Filtrage avec une clause .Where().
  • Tri avec différentes clauses .OrderBy().
  • Sélection des propriétés avec .Select().
  • Pagination avec .Skip() et .Take().

Compter les éléments d’une requête

Si vous avez besoin d’un nombre d’éléments retournés par la requête, vous pouvez utiliser .CountItemsAsync() sur une table ou .LongCountAsync() sur une requête :

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

Cette méthode entraîne un aller-retour vers le serveur. Vous pouvez également obtenir un nombre lors du remplissage d’une liste (par exemple), en évitant l’aller-retour supplémentaire :

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

Le nombre sera rempli après la première requête pour récupérer le contenu de la table.

Retour de toutes les données

Les données sont retournées via unIAsyncEnumerable :

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

Utilisez l’une des clauses de fin suivantes pour convertir le IAsyncEnumerable<T> en une autre collection :

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

En arrière-plan, la table distante gère la pagination du résultat pour vous. Tous les éléments sont retournés, quel que soit le nombre de requêtes côté serveur nécessaires pour répondre à la requête. Ces éléments sont également disponibles sur les résultats de la requête (par exemple, remoteTable.Where(m => m.Rating == "R")).

L’infrastructure de synchronisation des données fournit également ConcurrentObservableCollection<T> - une collection observable thread-safe. Cette classe peut être utilisée dans le contexte des applications d’interface utilisateur qui utilisent normalement ObservableCollection<T> pour gérer une liste (par exemple, Xamarin Forms ou mauI). Vous pouvez effacer et charger un ConcurrentObservableCollection<T> directement à partir d’une table ou d’une requête :

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

L’utilisation de .ToObservableCollection(collection) déclenche l’événement de CollectionChanged une fois pour l’ensemble de la collection plutôt que pour les éléments individuels, ce qui entraîne un redessinage plus rapide.

Le ConcurrentObservableCollection<T> a également des modifications basées sur les prédicats :

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

Les modifications basées sur les prédicats peuvent être utilisées dans les gestionnaires d’événements lorsque l’index de l’élément n’est pas connu à l’avance.

Filtrage des données

Vous pouvez utiliser une clause .Where() pour filtrer les données. Par exemple:

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

Le filtrage est effectué sur le service avant IAsyncEnumerable et sur le client après IAsyncEnumerable. Par exemple:

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

La première clause .Where() (renvoyer uniquement les éléments incomplets) est exécutée sur le service, tandis que la deuxième clause .Where() (commençant par « The ») est exécutée sur le client.

La clause Where prend en charge les opérations qui doivent être traduites dans le sous-ensemble OData. Les opérations incluent :

  • Opérateurs relationnels (==, !=, <, <=, >, >=),
  • Opérateurs arithmétiques (+, -, /, *, %),
  • Précision numérique (Math.Floor, Math.Ceiling),
  • Fonctions de chaîne (Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith) (cultures ordinales et invariantes uniquement) ;
  • Propriétés de date (Year, Month, Day, Hour, Minute, Second),
  • Accéder aux propriétés d’un objet et
  • Expressions combinant l’une de ces opérations.

Tri des données

Utilisez .OrderBy(), .OrderByDescending(), .ThenBy()et .ThenByDescending() avec un accesseur de propriété pour trier les données.

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

Le tri est effectué par le service. Vous ne pouvez pas spécifier d’expression dans une clause de tri. Si vous souhaitez trier par expression, utilisez le tri côté client :

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

Sélection des propriétés

Vous pouvez retourner un sous-ensemble de données à partir du service :

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

Retourner une page de données

Vous pouvez retourner un sous-ensemble du jeu de données à l’aide de .Skip() et de .Take() pour implémenter la pagination :

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

Dans une application réelle, vous pouvez utiliser des requêtes similaires à l’exemple précédent avec un contrôle de pagineur ou une interface utilisateur comparable pour naviguer entre les pages.

Toutes les fonctions décrites jusqu’à présent sont additifs. Nous pouvons donc continuer à les chaîner. Chaque appel chaîné affecte davantage la requête. Un autre exemple :

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

Rechercher des données distantes par ID

La fonction GetItemAsync peut être utilisée pour rechercher des objets à partir de la base de données avec un ID particulier.

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

Si l’élément que vous essayez de récupérer a été supprimé de manière réversible, vous devez utiliser le paramètre 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);

Insérer des données sur le serveur distant

Tous les types de clients doivent contenir un membre nommé ID, qui est par défaut une chaîne. Cet id est nécessaire pour effectuer des opérations CRUD et pour la synchronisation hors connexion. Le code suivant montre comment utiliser la méthode InsertItemAsync pour insérer de nouvelles lignes dans une table. Le paramètre contient les données à insérer en tant qu’objet .NET.

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

Si une valeur d’ID personnalisée unique n’est pas incluse dans le item pendant une insertion, le serveur génère un ID. Vous pouvez récupérer l’ID généré en inspectant l’objet après le retour de l’appel.

Mettre à jour les données sur le serveur distant

Le code suivant montre comment utiliser la méthode ReplaceItemAsync pour mettre à jour un enregistrement existant avec le même ID avec de nouvelles informations.

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

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

Supprimer des données sur le serveur distant

Le code suivant montre comment utiliser la méthode DeleteItemAsync pour supprimer une instance existante.

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

await todoTable.DeleteItemAsync(item);

Résolution des conflits et accès concurrentiel optimiste

Deux clients ou plus peuvent écrire des modifications dans le même élément en même temps. Sans détection de conflit, la dernière écriture remplacerait les mises à jour précédentes. contrôle d’accès concurrentiel optimiste suppose que chaque transaction peut valider et n’utilise donc aucun verrouillage de ressource. Le contrôle d’accès concurrentiel optimiste vérifie qu’aucune autre transaction n’a modifié les données avant de valider les données. Si les données ont été modifiées, la transaction est restaurée.

Azure Mobile Apps prend en charge le contrôle d’accès concurrentiel optimiste en suivant les modifications apportées à chaque élément à l’aide de la colonne de propriété système version définie pour chaque table de votre back-end Mobile App. Chaque fois qu’un enregistrement est mis à jour, Mobile Apps définit la propriété version pour cet enregistrement sur une nouvelle valeur. Pendant chaque demande de mise à jour, la propriété version de l’enregistrement inclus dans la requête est comparée à la même propriété pour l’enregistrement sur le serveur. Si la version passée avec la requête ne correspond pas au serveur principal, la bibliothèque cliente déclenche une exception DatasyncConflictException<T>. Le type inclus à l’exception est l’enregistrement du back-end contenant la version des serveurs de l’enregistrement. L’application peut ensuite utiliser ces informations pour décider s’il faut réexécuter la demande de mise à jour avec la valeur de version correcte du serveur principal pour valider les modifications.

L’accès concurrentiel optimiste est automatiquement activé lors de l’utilisation de l’objet de base DatasyncClientData.

Outre l’activation de l’accès concurrentiel optimiste, vous devez également intercepter l’exception DatasyncConflictException<T> dans votre code. Résolvez le conflit en appliquant la version correcte à l’enregistrement mis à jour, puis répétez l’appel avec l’enregistrement résolu. Le code suivant montre comment résoudre un conflit d’écriture une fois détecté :

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

Utiliser des tables hors connexion

Les tables hors connexion utilisent un magasin SQLite local pour stocker les données à utiliser en mode hors connexion. Toutes les opérations de table sont effectuées sur le magasin SQLite local au lieu du magasin de serveurs distants. Veillez à ajouter les Microsoft.Datasync.Client.SQLiteStore à chaque projet de plateforme et à tous les projets partagés.

Avant de créer une référence de table, le magasin local doit être préparé :

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

Une fois le magasin défini, vous pouvez créer le client :

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

Enfin, vous devez vous assurer que les fonctionnalités hors connexion sont initialisées :

await client.InitializeOfflineStoreAsync();

L’initialisation du magasin est normalement effectuée immédiatement après la création du client. L'OfflineConnectionString est un URI utilisé pour spécifier à la fois l’emplacement de la base de données SQLite et les options utilisées pour ouvrir la base de données. Pour plus d’informations, consultez noms de fichiers d’URI dans SQLite.

  • Pour utiliser un cache en mémoire, utilisez file:inmemory.db?mode=memory&cache=private.
  • Pour utiliser un fichier, utilisez file:/path/to/file.db

Vous devez spécifier le nom de fichier absolu pour le fichier. Si vous utilisez Xamarin, vous pouvez utiliser les Xamarin Essentials File System Helpers pour construire un chemin d’accès : Par exemple :

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

Si vous utilisez MAUI, vous pouvez utiliser les helpers du système de fichiers MAUI pour construire un chemin d’accès : Par exemple :

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

Créer une table hors connexion

Vous pouvez obtenir une référence de table à l’aide de la méthode GetOfflineTable<T> :

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

Comme pour la table distante, vous pouvez également exposer une table hors connexion en lecture seule :

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

Vous n’avez pas besoin de vous authentifier pour utiliser une table hors connexion. Vous devez uniquement vous authentifier lorsque vous communiquez avec le service principal.

Synchroniser une table hors connexion

Les tables hors connexion ne sont pas synchronisées avec le serveur principal par défaut. La synchronisation est divisée en deux parties. Vous pouvez envoyer des modifications séparément du téléchargement de nouveaux éléments. Par exemple:

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

Par défaut, toutes les tables utilisent la synchronisation incrémentielle : seuls les nouveaux enregistrements sont récupérés. Un enregistrement est inclus pour chaque requête unique (générée en créant un hachage MD5 de la requête OData).

Note

Le premier argument à PullItemsAsync est la requête OData qui indique les enregistrements à extraire sur l’appareil. Il est préférable de modifier le service pour renvoyer uniquement les enregistrements spécifiques à l’utilisateur plutôt que de créer des requêtes complexes côté client.

Les options (définies par l’objet PullOptions) n’ont généralement pas besoin d’être définies. Les options sont les suivantes :

  • PushOtherTables - si la valeur est true, toutes les tables sont envoyées (push).
  • QueryId - UN ID de requête spécifique à utiliser plutôt que celui généré.
  • WriteDeltaTokenInterval : fréquence d’écriture du jeton delta utilisé pour suivre la synchronisation incrémentielle.

Le Kit de développement logiciel (SDK) effectue une PushAsync() implicite avant l’extraction d’enregistrements.

La gestion des conflits se produit sur une méthode PullAsync(). Gérez les conflits de la même façon que les tables en ligne. Le conflit est généré lorsque PullAsync() est appelé au lieu de l’insertion, de la mise à jour ou de la suppression. Si plusieurs conflits se produisent, ils sont regroupés dans une seule PushFailedException. Gérez chaque défaillance séparément.

Envoyer (push) des modifications pour toutes les tables

Pour envoyer (push) toutes les modifications au serveur distant, utilisez :

await client.PushTablesAsync();

Pour envoyer (push) des modifications pour un sous-ensemble de tables, fournissez une IEnumerable<string> à la méthode PushTablesAsync() :

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

Utilisez la propriété client.PendingOperations pour lire le nombre d’opérations en attente d’envoi (push) vers le service distant. Cette propriété est null quand aucun magasin hors connexion n’a été configuré.

Exécuter des requêtes SQLite complexes

Si vous devez effectuer des requêtes SQL complexes sur la base de données hors connexion, vous pouvez le faire à l’aide de la méthode ExecuteQueryAsync(). Par exemple, pour effectuer une instruction SQL JOIN, définissez une JObject qui montre la structure de la valeur de retour, puis utilisez 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 définition est un ensemble de clés/valeurs. Les clés doivent correspondre aux noms de champs retournés par la requête SQL, et les valeurs doivent être la valeur par défaut du type attendu. Utilisez 0L pour les nombres (long), false pour les booléens et string.Empty pour tout le reste.

SQLite a un ensemble restrictif de types pris en charge. Les dates/heures sont stockées en tant que nombre de millisecondes depuis l’époque pour autoriser les comparaisons.

Authentifier les utilisateurs

Azure Mobile Apps vous permet de générer un fournisseur d’authentification pour gérer les appels d’authentification. Spécifiez le fournisseur d’authentification lors de la construction du client de service :

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

Chaque fois que l’authentification est requise, le fournisseur d’authentification est appelé pour obtenir le jeton. Un fournisseur d’authentification générique peut être utilisé pour l’authentification basée sur l’en-tête d’autorisation et l’authentification app Service et l’authentification basée sur l’autorisation. Utilisez le modèle suivant :

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

Les jetons d’authentification sont mis en cache en mémoire (jamais écrits sur l’appareil) et actualisés si nécessaire.

Utiliser la plateforme d’identités Microsoft

La plateforme d’identités Microsoft vous permet d’intégrer facilement l’ID Microsoft Entra. Consultez les didacticiels de démarrage rapide pour obtenir un didacticiel complet sur l’implémentation de l’authentification Microsoft Entra. Le code suivant montre un exemple de récupération du jeton d’accès :

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

Pour plus d’informations sur l’intégration de la plateforme d’identités Microsoft à ASP.NET 6, consultez la documentation plateforme d’identités Microsoft.

Utiliser Xamarin Essentials ou MAUI WebAuthenticator

Pour l’authentification Azure App Service, vous pouvez utiliser le Xamarin Essentials ou le MAUI WebAuthenticator pour obtenir un jeton :

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

Les UserId et les DisplayName ne sont pas directement disponibles lors de l’utilisation de l’authentification Azure App Service. Utilisez plutôt un demandeur différé pour récupérer les informations à partir du point de terminaison /.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);
    }
}

Rubriques avancées

Purger des entités dans la base de données locale

En cas d’opération normale, les entités de purge ne sont pas requises. Le processus de synchronisation supprime les entités supprimées et gère les métadonnées requises pour les tables de base de données locales. Toutefois, il est temps de purger des entités au sein de la base de données. L’un de ces scénarios est le cas où vous devez supprimer un grand nombre d’entités et qu’il est plus efficace de réinitialiser les données de la table localement.

Pour vider les enregistrements d’une table, utilisez table.PurgeItemsAsync():

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

La requête identifie les entités à supprimer de la table. Identifiez les entités à vider à l’aide de LINQ :

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

La classe PurgeOptions fournit des paramètres pour modifier l’opération de vidage :

  • DiscardPendingOperations ignore toutes les opérations en attente pour la table qui se trouvent dans la file d’attente des opérations en attente d’envoi au serveur.
  • QueryId spécifie un ID de requête utilisé pour identifier le jeton delta à utiliser pour l’opération.
  • TimestampUpdatePolicy spécifie comment ajuster le jeton delta à la fin de l’opération de vidage :
    • TimestampUpdatePolicy.NoUpdate indique que le jeton delta ne doit pas être mis à jour.
    • TimestampUpdatePolicy.UpdateToLastEntity indique que le jeton delta doit être mis à jour vers le champ updatedAt pour la dernière entité stockée dans la table.
    • TimestampUpdatePolicy.UpdateToNow indique que le jeton delta doit être mis à jour à la date/heure actuelle.
    • TimestampUpdatePolicy.UpdateToEpoch indique que le jeton delta doit être réinitialisé pour synchroniser toutes les données.

Utilisez la même valeur QueryId utilisée lors de l’appel de table.PullItemsAsync() pour synchroniser les données. Le QueryId spécifie le jeton delta à mettre à jour une fois le vidage terminé.

Personnaliser les en-têtes de requête

Pour prendre en charge votre scénario d’application spécifique, vous devrez peut-être personnaliser la communication avec le serveur principal de l’application mobile. Par exemple, vous pouvez ajouter un en-tête personnalisé à chaque demande sortante ou modifier les codes d’état de réponse avant de revenir à l’utilisateur. Utilisez un DelegatingHandler personnalisé, comme dans l’exemple suivant :

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

Activer la journalisation des demandes

Vous pouvez également utiliser un Gestionnaire de délégation pour ajouter la journalisation des demandes :

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

Surveiller les événements de synchronisation

Lorsqu’un événement de synchronisation se produit, l’événement est publié sur le délégué d’événement client.SynchronizationProgress. Les événements peuvent être utilisés pour surveiller la progression du processus de synchronisation. Définissez un gestionnaire d’événements de synchronisation comme suit :

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

Le type SynchronizationEventArgs est défini comme suit :

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

Les propriétés dans args sont null ou -1 lorsque la propriété n’est pas pertinente pour l’événement de synchronisation.