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

Ce guide vous montre comment traiter des scénarios courants mettant en jeu 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 :

Remarque

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).
  • plateforme Windows universelle builds 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 l’URL du back-end 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 de 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 la 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 jeu est défini, un en-tête X-ZUMO-INSTALLATION-ID personnalisé 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

Il 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.

Sérialiseur Paramètres

Si vous avez modifié les paramètres du sérialiseur sur le serveur de synchronisation des données, vous devez apporter les mêmes modifications au SerializerSettings 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 /tables/{tableName} chemin d’accès (comme spécifié par l’attribut dans le Route code du serveur). Toutefois, les tables peuvent exister sur n’importe quel chemin de point de terminaison. Il TableEndpointResolver s’agit d’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 soient situées 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 UserAgent propriété 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 traité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 IReadOnlyRemoteTable<T> version :

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

Le type de modèle doit implémenter le ITableData contrat à partir du service. Permet DatasyncClientData de 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 pour 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 une précision ms.
  • System.DateTimeOffset - en tant que chaîne de date/heure UTC ISO-8601 avec une 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 .Where() clause.
  • Tri avec différentes .OrderBy() clauses.
  • 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 un IAsyncEnumerable :

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> fichier 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 ConcurrentObservableCollection<T> également une collection observable thread-safe. Cette classe peut être utilisée dans le contexte des applications d’interface utilisateur qui utiliseraient normalement ObservableCollection<T> pour gérer une liste (par exemple, les listes Xamarin Forms ou MAUI). Vous pouvez effacer et charger un ConcurrentObservableCollection<T> fichier directement à partir d’une table ou d’une requête :

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

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

Le ConcurrentObservableCollection<T> prédicat 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 .Where() clause 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 .Where() clause (renvoyer uniquement les éléments incomplets) est exécutée sur le service, tandis que la deuxième .Where() clause (commençant par « The ») est exécutée sur le client.

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

  • des opérateurs relationnels (==, !=, <, <=, >, >=),
  • des opérateurs arithmétiques (+, -, /, *, %),
  • la précision des nombres (Math.Floor, Math.Ceiling),
  • Fonctions de chaîne (Length, , Substring, IndexOfReplace, StartsWithEquals, EndsWith) (cultures ordinales et invariantes uniquement),
  • des propriétés de date (Year, Month, Day, Hour, Minute, Second),
  • les propriétés d’accès d’un objet, ainsi que
  • les expressions qui combinent toutes 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 .Skip() et .Take() implémenter la pagination :

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

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

Toutes les fonctions décrites jusqu’ici étant cumulatives, nous pouvons les concaténer. Chaque appel chaîné aura plus de répercussions sur la requête. Un exemple supplémentaire :

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 permet de rechercher des objets dans la base de données à partir d'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 includeDeleted paramètre :

// 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 clients doivent contenir un membre nommé Id, 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 InsertItemAsync méthode pour insérer de nouvelles lignes dans une table. Le paramètre contient les données à insérer sous forme d'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 l’insertion item , le serveur génère un ID. Vous pouvez récupérer l’ID généré en examinant 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 ReplaceItemAsync méthode 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 DeleteItemAsync méthode 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. En l'absence de détection de conflits, la dernière écriture remplace les mises à jour précédentes. Le 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 effectuant le suivi des modifications apportées à chaque élément à l’aide de la version colonne de propriétés système définie pour chaque table de votre back-end Mobile App. Chaque fois qu’un enregistrement est mis à jour, Mobile Apps attribue une nouvelle valeur à la propriété version de cet enregistrement. À chaque demande de mise à jour, la propriété version de l'enregistrement inclus dans la demande est comparée à celle de l'enregistrement basé sur le serveur. Si la version passée avec la requête ne correspond pas au back-end, la bibliothèque cliente déclenche une DatasyncConflictException<T> exception. Le type inclus avec l’exception est l’enregistrement du backend contenant la version serveur de l’enregistrement. À partir de cette information, l’application peut décider ou non d’exécuter à nouveau la requête de mise à jour avec la valeur version correcte du serveur principal pour valider les modifications.

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

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 le correct version à 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 qu’il est 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 une banque de données SQLite locale pour stocker les données pour une utilisation en mode hors connexion. Toutes les opérations de table sont effectuées sur la banque de données SQLite locale au lieu de la banque de données du serveur distant. Veillez à ajouter le Microsoft.Datasync.Client.SQLiteStore projet à chaque projet de plateforme et à tous les projets partagés.

Avant que vous ne puissiez créer une référence de table, la banque de données locale doit être préparée :

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 de la banque de données est normalement effectuée immédiatement après la création du client. Offline Connecter ionString 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 helpers du système de fichiers Xamarin Essentials 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

Une référence de table peut être obtenue à l’aide de la GetOfflineTable<T> méthode :

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 transmettre des modifications en dehors du processus de 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).

Remarque

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. Options disponibles :

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

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

La gestion des conflits s’effectue par le biais d’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ée à la place de ou pendant l’insertion, la mise à jour ou la suppression. Si plusieurs conflits se produisent, ils sont regroupés en un seul 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>PushTablesAsync() méthode :

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

Utilisez la client.PendingOperations propriété 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 avez besoin d’effectuer des requêtes SQL complexes sur la base de données hors connexion, vous pouvez le faire à l’aide de la ExecuteQueryAsync() méthode. Par exemple, pour effectuer une SQL JOIN instruction, définissez une JObject 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 (longs), 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 des 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 le Plateforme d'identités Microsoft

Le 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 du 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 Xamarin Essentials WebAuthenticator ou 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
    };
}

Le UserId service Azure DisplayName App Service n’est pas directement disponible 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 /.auth/me point de terminaison :

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 PurgeOptions classe fournit des paramètres pour modifier l’opération de vidage :

  • DiscardPendingOperationsdis carte toutes les opérations en attente pour la table qui se trouvent dans la file d’attente des opérations en attente d’être envoyées 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 updatedAt champ de 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 QueryId valeur que celle que vous avez utilisée lors de l’appel table.PullItemsAsync() pour synchroniser les données. Spécifie QueryId le jeton delta à mettre à jour lorsque le vidage est terminé.

Personnaliser des en-têtes de demande

Pour prendre en charge votre scénario d’application en particulier, vous devrez peut-être personnaliser la communication avec le backend Mobile Apps. 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 gestionnaire de délégation 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 DelegatingHandler 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 à l’intérieur args sont soit null-1 lorsque la propriété n’est pas pertinente pour l’événement de synchronisation.