Uso de la biblioteca cliente de Azure Mobile Apps para .NET

Esta guía le indica cómo enfrentarse a determinadas situaciones habituales usando la biblioteca cliente de .NET para Azure Mobile Apps. Use la biblioteca cliente de .NET en cualquier aplicación de .NET 6 o .NET Standard 2.0, incluidos MAUI, Xamarin y Windows (WPF, UWP y WinUI).

Si no está familiarizado con Azure Mobile Apps, considere la posibilidad de completar primero uno de los tutoriales de inicio rápido:

Nota:

En este artículo se describe la edición más reciente (v6.0) de Microsoft Datasync Framework. Para clientes más antiguos, consulte la documentación de v4.2.0.

Plataformas compatibles

La biblioteca cliente de .NET admite cualquier plataforma de .NET Standard 2.0 o .NET 6, entre las que se incluyen:

  • .NET MAUI para plataformas Android, iOS y Windows.
  • Nivel de API de Android 21 y versiones posteriores (Xamarin y Android para .NET).
  • iOS versión 12.0 y posteriores (Xamarin e iOS para .NET).
  • Plataforma universal de Windows compila 19041 y versiones posteriores.
  • Windows Presentation Framework (WPF).
  • SDK de Aplicaciones para Windows (WinUI 3).
  • Xamarin.Forms

Además, se han creado ejemplos para Avalonia y Uno Platform. El ejemplo TodoApp contiene un ejemplo de cada plataforma probada.

Configuración y requisitos previos

Agregue las siguientes bibliotecas desde NuGet:

Si usa un proyecto de plataforma (por ejemplo, .NET MAUI), asegúrese de agregar las bibliotecas al proyecto de plataforma y a cualquier proyecto compartido.

Creación del cliente de servicio

El código siguiente crea el cliente de servicio, que se usa para coordinar toda la comunicación con las tablas back-end y sin conexión.

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

En el código anterior, reemplace por MOBILE_APP_URL la dirección URL del back-end de ASP.NET Core. El cliente debe crearse como singleton. Si usa un proveedor de autenticación, se puede configurar de la siguiente manera:

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

Más adelante en este documento se proporcionan más detalles sobre el proveedor de autenticación.

Opciones

Se puede crear un conjunto completo (predeterminado) de opciones como este:

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

Normalmente, se realiza una solicitud HTTP pasando la solicitud a través del proveedor de autenticación (que agrega el Authorization encabezado para el usuario autenticado actualmente) antes de enviar la solicitud. Opcionalmente, puede agregar más controladores de delegación. Cada solicitud pasa por los controladores de delegación antes de enviarlos al servicio. Los controladores de delegación permiten agregar encabezados adicionales, realizar reintentos o proporcionar funcionalidades de registro.

Se proporcionan ejemplos de controladores de delegación para registrar y agregar encabezados de solicitud más adelante en este artículo.

IdGenerator

Cuando se agrega una entidad a una tabla sin conexión, debe tener un identificador. Se genera un identificador si no se proporciona uno. La IdGenerator opción permite personalizar el identificador generado. De forma predeterminada, se genera un identificador único global. Por ejemplo, la siguiente configuración genera una cadena que incluye el nombre de la tabla y un GUID:

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

InstallationId

Si se establece un InstallationId , se envía un encabezado X-ZUMO-INSTALLATION-ID personalizado con cada solicitud para identificar la combinación de la aplicación en un dispositivo específico. Este encabezado se puede registrar en los registros y permite determinar el número de instalaciones distintas para la aplicación. Si usa InstallationId, el identificador debe almacenarse en el almacenamiento persistente en el dispositivo para que se pueda realizar un seguimiento de las instalaciones únicas.

OfflineStore

OfflineStore se usa al configurar el acceso a datos sin conexión. Para obtener más información, consulte Trabajar con tablas sin conexión.

ParallelOperations

Parte del proceso de sincronización sin conexión implica insertar operaciones en cola en el servidor remoto. Cuando se desencadena la operación de inserción, las operaciones se envían en el orden en que se recibieron. Opcionalmente, puede usar hasta ocho subprocesos para insertar estas operaciones. Las operaciones paralelas usan más recursos tanto en el cliente como en el servidor para completar la operación más rápido. El orden en el que las operaciones llegan al servidor no se pueden garantizar al usar varios subprocesos.

Serializador Configuración

Si ha cambiado la configuración del serializador en el servidor de sincronización de datos, debe realizar los mismos cambios en en el SerializerSettings cliente. Esta opción le permite especificar su propia configuración de serializador.

TableEndpointResolver

Por convención, las tablas se encuentran en el servicio remoto en la /tables/{tableName} ruta de acceso (según lo especificado por el Route atributo en el código del servidor). Sin embargo, las tablas pueden existir en cualquier ruta de acceso del punto de conexión. TableEndpointResolver es una función que convierte un nombre de tabla en una ruta de acceso para comunicarse con el servicio remoto.

Por ejemplo, lo siguiente cambia la suposición para que todas las tablas se encuentren en /api:

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

UserAgent

El cliente de sincronización de datos genera un valor de encabezado user-Agent adecuado en función de la versión de la biblioteca. Algunos desarrolladores sienten que el encabezado del agente de usuario pierde información sobre el cliente. Puede establecer la UserAgent propiedad en cualquier valor de encabezado válido.

Trabajar con tablas remotas

En la sección siguiente se detalla cómo buscar y recuperar registros y modificar los datos dentro de una tabla remota. Se tratan los temas siguientes:

Creación de una referencia de tabla remota

Para crear una referencia de tabla remota, use GetRemoteTable<T>:

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

Si desea devolver una tabla de solo lectura, use la IReadOnlyRemoteTable<T> versión:

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

El tipo de modelo debe implementar el ITableData contrato desde el servicio. Use DatasyncClientData para proporcionar los campos necesarios:

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

El DatasyncClientData objeto incluye:

  • Id (string): un identificador único global para el elemento.
  • UpdatedAt (System.DataTimeOffset): fecha y hora en que se actualizó por última vez el elemento.
  • Version (string): una cadena opaca que se usa para el control de versiones.
  • Deleted (booleano): si truees , se elimina el elemento.

El servicio mantiene estos campos. No ajuste estos campos como parte de la aplicación cliente.

Los modelos se pueden anotar mediante atributos Newtonsoft.JSON. El nombre de la tabla se puede especificar mediante el DataTable atributo :

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

Como alternativa, especifique el nombre de la tabla en la GetRemoteTable() llamada:

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

El cliente usa la ruta de acceso /tables/{tablename} como URI. El nombre de la tabla también es el nombre de la tabla sin conexión de la base de datos de SQLite.

Tipos admitidos

Aparte de los tipos primitivos (int, float, string, etc.), se admiten los siguientes tipos para los modelos:

  • System.DateTime - como cadena de fecha y hora UTC ISO-8601 con precisión ms.
  • System.DateTimeOffset - como cadena de fecha y hora UTC ISO-8601 con precisión ms.
  • System.Guid - con formato de 32 dígitos separados como guiones.

Consulta de datos desde un servidor remoto

La tabla remota se puede usar con instrucciones de tipo LINQ, entre las que se incluyen:

  • Filtrado con una .Where() cláusula .
  • Ordenación con varias .OrderBy() cláusulas.
  • Selección de propiedades con .Select().
  • Paginación con .Skip() y .Take().

Recuento de elementos de una consulta

Si necesita un recuento de los elementos que devolvería la consulta, puede usar .CountItemsAsync() en una tabla o .LongCountAsync() en una consulta:

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

Este método provoca un recorrido de ida y vuelta al servidor. También puede obtener un recuento al rellenar una lista (por ejemplo), evitando el recorrido de ida y vuelta adicional:

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

El recuento se rellenará después de la primera solicitud para recuperar el contenido de la tabla.

Devolver todos los datos

Los datos se devuelven a través de IAsyncEnumerable:

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

Use cualquiera de las siguientes cláusulas de terminación para convertir en IAsyncEnumerable<T> una colección diferente:

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 segundo plano, la tabla remota controla la paginación del resultado automáticamente. Todos los elementos se devuelven independientemente del número de solicitudes del lado servidor necesarias para cumplir la consulta. Estos elementos también están disponibles en los resultados de la consulta (por ejemplo, remoteTable.Where(m => m.Rating == "R")).

El marco de sincronización de datos también proporciona ConcurrentObservableCollection<T> una colección observable segura para subprocesos. Esta clase se puede usar en el contexto de las aplicaciones de interfaz de usuario que normalmente usarían ObservableCollection<T> para administrar una lista (por ejemplo, listas de Xamarin Forms o MAUI). Puede borrar y cargar directamente ConcurrentObservableCollection<T> desde una tabla o consulta:

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

El uso .ToObservableCollection(collection) de desencadena el CollectionChanged evento una vez para toda la colección en lugar de para elementos individuales, lo que da lugar a un tiempo de volver a dibujar más rápido.

ConcurrentObservableCollection<T> También tiene modificaciones controladas por predicados:

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

Las modificaciones controladas por predicados se pueden usar en controladores de eventos cuando el índice del elemento no se conoce de antemano.

Filtrado de datos

Puede usar una .Where() cláusula para filtrar los datos. Por ejemplo:

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

El filtrado se realiza en el servicio antes de IAsyncEnumerable y en el cliente después de IAsyncEnumerable. Por ejemplo:

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

La primera .Where() cláusula (devolver solo elementos incompletos) se ejecuta en el servicio, mientras que la segunda .Where() cláusula (a partir de "The") se ejecuta en el cliente.

La cláusula Where admite las operaciones que pueden traducirse en el subconjunto OData. Estas son algunas de las operaciones:

  • Operadores relacionales (==, !=, <, <=, >, >=),
  • Operadores aritméticos (+, -, /, *, %),
  • Precisión de número (Math.Floor, Math.Ceiling),
  • Funciones de cadena (Length, Substring, Replace, IndexOf, Equals, StartsWith) EndsWith(solo referencias culturales ordinales e invariables),
  • Propiedades de fecha (Year, Month, Day, Hour, Minute, Second),
  • Propiedades de acceso de un objeto
  • Expresiones combinando cualquiera de estas operaciones

Ordenar datos

Use .OrderBy(), .OrderByDescending(), .ThenBy()y .ThenByDescending() con un descriptor de acceso de propiedad para ordenar los datos.

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

El servicio realiza la ordenación. No se puede especificar una expresión en ninguna cláusula de ordenación. Si desea ordenar por una expresión, use la ordenación del lado cliente:

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

Selección de propiedades

Puede devolver un subconjunto de datos del servicio:

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

Devolver una página de datos

Puede devolver un subconjunto del conjunto de datos mediante .Skip() e .Take() implementar la paginación:

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

En una aplicación real, puede usar consultas similares a las anteriores con un control de paginación o una interfaz de usuario comparable para permitir a los usuarios desplazarse entre las páginas.

Todas las funciones descritas hasta ahora son aditivas, por lo que podemos mantener el encadenamiento. Cada llamada encadenada afecta a más elementos aparte de la consulta. A continuación se muestra un ejemplo más:

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

Búsqueda de datos remotos por identificador

La función GetItemAsync puede usarse para buscar objetos desde la base de datos con un identificador determinado.

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

Si el elemento que intenta recuperar se ha eliminado temporalmente, debe usar el includeDeleted parámetro :

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

Insertar datos en el servidor remoto

Todos los tipos de cliente deben incluir un miembro llamado Id, que es una cadena de forma predeterminada. Este identificador es necesario para realizar operaciones CRUD y para la sincronización sin conexión. En el código siguiente se muestra cómo usar el InsertItemAsync método para insertar nuevas filas en una tabla. El parámetro contiene los datos que se van a insertar como un objeto .NET.

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

Si no se incluye un valor de identificador personalizado único en item durante una inserción, el servidor genera un identificador. Puede recuperar el identificador generado inspeccionando el objeto después de que se devuelva la llamada.

Actualizar datos en el servidor remoto

En el código siguiente se muestra cómo usar el ReplaceItemAsync método para actualizar un registro existente con el mismo identificador con nueva información.

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

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

Eliminación de datos en el servidor remoto

En el código siguiente se muestra cómo usar el DeleteItemAsync método para eliminar una instancia existente.

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

await todoTable.DeleteItemAsync(item);

Resolución de conflictos y simultaneidad optimista

Dos o más clientes pueden escribir cambios en el mismo elemento al mismo tiempo. Si no se produjera la detección de conflictos, la última escritura sobrescribiría cualquier actualización anterior. El control de simultaneidad optimista supone que cada transacción puede confirmarse y, por lo tanto, no usa ningún bloqueo de recursos. El control de simultaneidad optimista comprueba que ninguna otra transacción ha modificado los datos antes de confirmarlos. Si se han modificado los datos, la transacción se revierte.

Azure Mobile Apps admite el control de simultaneidad optimista mediante el seguimiento de los cambios en cada elemento mediante la columna de propiedades del sistema definida para cada tabla en el version back-end de la aplicación móvil. Cada vez que se actualiza un registro, el servicio Mobile Apps establece la propiedad version de ese registro en un nuevo valor. Durante cada solicitud de actualización, la propiedad version del registro incluido con la solicitud se compara con la misma propiedad del registro en el servidor. Si la versión pasada con la solicitud no coincide con el back-end, la biblioteca cliente genera una DatasyncConflictException<T> excepción. El tipo incluido con la excepción es el registro del back-end que contiene la versión del registro del servidor. A continuación, la aplicación puede usar esta información para decidir si ejecutar la solicitud de actualización de nuevo con el valor version correcto del back-end para confirmar los cambios.

La simultaneidad optimista se habilita automáticamente al usar el DatasyncClientData objeto base.

Además de habilitar la simultaneidad optimista, también debe detectar la excepción en el DatasyncConflictException<T> código. Resuelva el conflicto aplicando el correcto version al registro actualizado y repita la llamada con el registro resuelto. El siguiente código muestra cómo resolver un conflicto de escritura detectado:

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

Trabajar con tablas sin conexión

Las tablas sin conexión utilizan un almacén SQLite local para almacenar datos para usarlos cuando estén sin conexión. Todas las operaciones de las tablas se realizan en el almacén SQLite local, en lugar del almacén del servidor remoto. Asegúrese de agregar a Microsoft.Datasync.Client.SQLiteStore cada proyecto de plataforma y a los proyectos compartidos.

Para poder crear una referencia de tabla, debe prepararse el almacén local:

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

Una vez definido el almacén, puede crear el cliente:

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

Por último, debe asegurarse de que se inicializan las funcionalidades sin conexión:

await client.InitializeOfflineStoreAsync();

Normalmente, la inicialización del almacén se realiza inmediatamente después de que se crea el cliente. Offline Conectar ionString es un URI que se usa para especificar tanto la ubicación de la base de datos de SQLite como las opciones usadas para abrir la base de datos. Para obtener más información, consulte Nombres de archivo de URI en SQLite.

  • Para usar una caché en memoria, use file:inmemory.db?mode=memory&cache=private.
  • Para usar un archivo, use file:/path/to/file.db

Debe especificar el nombre de archivo absoluto para el archivo. Si usa Xamarin, puede usar los asistentes del sistema de archivos de Xamarin Essentials para construir una ruta de acceso: Por ejemplo:

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

Si usa MAUI, puede usar los asistentes del sistema de archivos MAUI para construir una ruta de acceso: Por ejemplo:

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

Creación de una tabla sin conexión

Un referencia de tabla se puede obtener mediante el método GetOfflineTable<T>:

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

Al igual que con la tabla remota, también puede exponer una tabla sin conexión de solo lectura:

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

No es necesario autenticarse para usar una tabla sin conexión. Solo tiene que autenticarse cuando se comunica con el servicio back-end.

Sincronizar una tabla sin conexión

Las tablas sin conexión no se sincronizan con el back-end de forma predeterminada. La sincronización se divide en dos partes. Mediante la descarga de elementos nuevos puede insertar los cambios por separado. Por ejemplo:

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

De forma predeterminada, todas las tablas usan sincronización incremental: solo se recuperan nuevos registros. Se incluye un registro para cada consulta única (generada mediante la creación de un hash MD5 de la consulta OData).

Nota:

El primer argumento para PullItemsAsync es la consulta OData que indica qué registros se van a extraer del dispositivo. Es mejor modificar el servicio para que solo devuelva registros específicos del usuario en lugar de crear consultas complejas en el lado cliente.

Por lo general, no es necesario establecer las opciones (definidas por el PullOptions objeto). Entre las opciones se incluyen:

  • PushOtherTables : si se establece en true, se insertan todas las tablas.
  • QueryId : un identificador de consulta específico que se va a usar en lugar del generado.
  • WriteDeltaTokenInterval : frecuencia con la que se escribe el token delta usado para realizar un seguimiento de la sincronización incremental.

El SDK realiza una tarea PushAsync() implícita antes de extraer registros.

El control de los conflictos se realiza en un método PullAsync(). Controle los conflictos de la misma manera que las tablas en línea. El conflicto se produce cuando se llama a PullAsync(), en lugar de durante la inserción, actualización o eliminación. Si se producen varios conflictos, se agrupan en un único PushFailedException. Trate cada error por separado.

Inserción de cambios para todas las tablas

Para insertar todos los cambios en el servidor remoto, use:

await client.PushTablesAsync();

Para insertar cambios en un subconjunto de tablas, proporcione un IEnumerable<string> al PushTablesAsync() método :

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

Use la client.PendingOperations propiedad para leer el número de operaciones en espera de insertarse en el servicio remoto. Esta propiedad es null cuando no se ha configurado ningún almacén sin conexión.

Ejecución de consultas complejas de SQLite

Si necesita realizar consultas SQL complejas en la base de datos sin conexión, puede hacerlo mediante el ExecuteQueryAsync() método . Por ejemplo, para realizar una SQL JOIN instrucción , defina un JObject que muestre la estructura del valor devuelto y, a continuación, use 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 definición es un conjunto de claves y valores. Las claves deben coincidir con los nombres de campo que devuelve la consulta SQL y los valores deben ser el valor predeterminado del tipo esperado. Use 0L para números (long), false para booleanos y string.Empty para todo lo demás.

SQLite tiene un conjunto restrictivo de tipos admitidos. Fecha y hora se almacenan como el número de milisegundos desde la época para permitir comparaciones.

Autenticar usuarios

Azure Mobile Apps permite generar un proveedor de autenticación para controlar las llamadas de autenticación. Especifique el proveedor de autenticación al construir el cliente de servicio:

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

Siempre que se requiera la autenticación, se llama al proveedor de autenticación para obtener el token. Un proveedor de autenticación genérico se puede usar tanto para la autenticación basada en encabezados de autorización como para la autenticación basada en la autenticación de App Service y la autenticación basada en autorización. Use el siguiente modelo:

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

Los tokens de autenticación se almacenan en caché en memoria (nunca se escriben en el dispositivo) y se actualizan cuando sea necesario.

Usar el Plataforma de identidad de Microsoft

El Plataforma de identidad de Microsoft permite integrar fácilmente con microsoft Entra ID. Consulte los tutoriales de inicio rápido para ver un tutorial completo sobre cómo implementar la autenticación de Microsoft Entra. En el código siguiente se muestra un ejemplo de recuperación del token de acceso:

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

Para obtener más información sobre la integración del Plataforma de identidad de Microsoft con ASP.NET 6, consulte la documentación de Plataforma de identidad de Microsoft.

Uso de Xamarin Essentials o MAUI WebAuthenticator

Para la autenticación del servicio App de Azure, puede usar Xamarin Essentials WebAuthenticator o MAUI WebAuthenticator para obtener 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
    };
}

y UserIdDisplayName no están disponibles directamente al usar App de Azure Autenticación de servicio. En su lugar, use un solicitante diferido para recuperar la información del /.auth/me punto de conexión:

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

Temas avanzados

Purga de entidades en la base de datos local

En funcionamiento normal, no se requieren entidades de purga. El proceso de sincronización quita las entidades eliminadas y mantiene los metadatos necesarios para las tablas de base de datos locales. Sin embargo, hay ocasiones en las que la purga de entidades dentro de la base de datos resulta útil. Uno de estos escenarios es cuando necesita eliminar un gran número de entidades y es más eficaz borrar datos de la tabla localmente.

Para purgar registros de una tabla, use table.PurgeItemsAsync():

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

La consulta identifica las entidades que se van a quitar de la tabla. Identifique las entidades que se van a purgar mediante LINQ:

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

La PurgeOptions clase proporciona la configuración para modificar la operación de purga:

  • DiscardPendingOperations descarta las operaciones pendientes de la tabla que se encuentran en la cola de operaciones en espera de enviarse al servidor.
  • QueryId especifica un identificador de consulta que se usa para identificar el token delta que se usará para la operación.
  • TimestampUpdatePolicy especifica cómo ajustar el token delta al final de la operación de purga:
    • TimestampUpdatePolicy.NoUpdate indica que el token delta no debe actualizarse.
    • TimestampUpdatePolicy.UpdateToLastEntity indica que el token delta debe actualizarse al updatedAt campo de la última entidad almacenada en la tabla.
    • TimestampUpdatePolicy.UpdateToNow indica que el token delta debe actualizarse a la fecha y hora actuales.
    • TimestampUpdatePolicy.UpdateToEpoch indica que el token delta debe restablecerse para sincronizar todos los datos.

Use el mismo QueryId valor que usó al llamar table.PullItemsAsync() a para sincronizar los datos. QueryId especifica el token delta que se va a actualizar cuando se completa la purga.

Personalización de encabezados de solicitud

Para admitir su escenario de aplicación específico, deberá personalizar la comunicación con el back-end de la aplicación móvil. Por ejemplo, puede agregar un encabezado personalizado a cada solicitud saliente o cambiar los códigos de estado de respuesta antes de volver al usuario. Use un DelegatingHandler personalizado, como en el ejemplo siguiente:

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

Habilitación del registro de solicitudes

También puede usar DelegatingHandler para agregar el registro de solicitudes:

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

Supervisión de eventos de sincronización

Cuando se produce un evento de sincronización, el evento se publica en el delegado de client.SynchronizationProgress eventos. Los eventos se pueden usar para supervisar el progreso del proceso de sincronización. Defina un controlador de eventos de sincronización como se indica a continuación:

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

El tipo SynchronizationEventArgs se define de la siguiente manera:

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

Las propiedades dentro args de son null o -1 cuando la propiedad no es relevante para el evento de sincronización.