Compartir a través de


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

Nota

Este producto se retira. Para obtener un reemplazo de proyectos con .NET 8 o posterior, consulte la biblioteca datasync de Community Toolkit.

En esta guía se muestra cómo realizar escenarios comunes mediante 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 admitidas

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).
  • La Plataforma universal de Windows compila 19041 y versiones posteriores.
  • Windows Presentation Framework (WPF).
  • SDK de aplicaciones de Windows (WinUI 3).
  • Xamarin.Forms

Además, se han creado ejemplos para Avalonia y Uno Platform. El de 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 MOBILE_APP_URL por 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 encabezado Authorization 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 de registro 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 opción IdGenerator permite adaptar el identificador que se genera. 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 personalizado X-ZUMO-INSTALLATION-ID 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

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

SerializerSettings

Si ha cambiado la configuración del serializador en el servidor de sincronización de datos, debe realizar los mismos cambios en el SerializerSettings en el 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 ruta de acceso /tables/{tableName} (según lo especificado por el atributo Route en el código de servidor). Sin embargo, las tablas pueden existir en cualquier ruta de acceso del punto de conexión. El 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 propiedad UserAgent 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 versión de IReadOnlyRemoteTable<T>:

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

El tipo de modelo debe implementar el contrato de ITableData 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 objeto DatasyncClientData incluye:

  • Id (cadena): un identificador único global para el elemento.
  • UpdatedAt (System.DataTimeOffset): fecha y hora en que se actualizó por última vez el elemento.
  • Version (cadena): una cadena opaca que se usa para el control de versiones.
  • Deleted (booleano): si true, 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 de Newtonsoft.JSON. El nombre de la tabla se puede especificar mediante el atributo DataTable:

[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 llamada GetRemoteTable():

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, etcetera).), se admiten los siguientes tipos para los modelos:

  • System.DateTime: como una cadena de fecha y hora UTC ISO-8601 con precisión ms.
  • System.DateTimeOffset: como una 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 cláusula .Where().
  • Ordenación con varias cláusulas de .OrderBy().
  • 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 unIAsyncEnumerable :

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 el IAsyncEnumerable<T> en 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 un ConcurrentObservableCollection<T> directamente desde una tabla o consulta:

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

El uso de .ToObservableCollection(collection) desencadena el evento CollectionChanged 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.

El 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 cláusula .Where() 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 cláusula .Where() (devolver solo elementos incompletos) se ejecuta en el servicio, mientras que la segunda cláusula .Where() (a partir de "The") se ejecuta en el cliente.

La cláusula Where admite operaciones que se traducen en el subconjunto de OData. Las operaciones incluyen:

  • Operadores relacionales (==, !=, <, <=, >, >=),
  • Operadores aritméticos (+, -, /, *, %),
  • Precisión del número (Math.Floor, Math.Ceiling),
  • Funciones de cadena (Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith) (solo referencias culturales ordinales e invariables),
  • Propiedades date (Year, Month, Day, Hour, Minute, Second),
  • Obtener acceso a las propiedades de un objeto y
  • Expresiones que combinan 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() y .Take() para implementar la paginación:

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

En una aplicación real, puede usar consultas similares al ejemplo anterior con un control de buscapersonas o una interfaz de usuario comparable para navegar entre páginas.

Todas las funciones descritas hasta ahora son sumas, por lo que podemos encadenarlas. Cada llamada encadenada afecta a más de la consulta. 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 se puede usar para buscar objetos de 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 parámetro 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);

Insertar datos en el servidor remoto

Todos los tipos de cliente deben contener un miembro denominado Id, que es de forma predeterminada una cadena. Este id. 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 método InsertItemAsync 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 el 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 método ReplaceItemAsync 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 método DeleteItemAsync 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. Sin la detección de conflictos, la última escritura sobrescribiría las actualizaciones anteriores. 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 version que se define para cada tabla del back-end de la aplicación móvil. Cada vez que se actualiza un registro, Mobile Apps establece la propiedad version para 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 para el registro en el servidor. Si la versión pasada con la solicitud no coincide con el back-end, la biblioteca cliente genera una excepción de DatasyncConflictException<T>. El tipo incluido con la excepción es el registro del back-end que contiene la versión de los servidores del registro. Después, la aplicación puede usar esta información para decidir si debe ejecutar la solicitud de actualización de nuevo con el valor de version correcto del back-end para confirmar los cambios.

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

Además de habilitar la simultaneidad optimista, también debe detectar la excepción DatasyncConflictException<T> en el código. Resuelva el conflicto aplicando el version correcto al registro actualizado y repita la llamada con el registro resuelto. El código siguiente muestra cómo resolver un conflicto de escritura una vez 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 usan un almacén de SQLite local para almacenar datos para usarlos cuando están sin conexión. Todas las operaciones de tabla se realizan en el almacén de SQLite local en lugar del almacén del servidor remoto. Asegúrese de agregar el Microsoft.Datasync.Client.SQLiteStore a cada proyecto de plataforma y a los proyectos compartidos.

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

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 de la tienda se realiza inmediatamente después de crear el cliente. El offlineConnectionString 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, vea URI Filenames in 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 las 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

Se puede obtener una referencia de tabla 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. Puede insertar cambios por separado de la descarga de nuevos elementos. 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 los registros que 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 objeto PullOptions). 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 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 PushAsync() implícita antes de extraer registros.

El control de conflictos se produce 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 una sola PushFailedException. Controle 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 método PushTablesAsync():

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

Use la propiedad client.PendingOperations para leer el número de operaciones que esperan 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 método ExecuteQueryAsync(). Por ejemplo, para realizar una instrucción SQL JOIN, 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.

Autenticación de 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.

Uso de la plataforma de identidad de Microsoft

La plataforma de identidad de Microsoft le 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 de la 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 de Azure App Service, puede usar el webAuthenticator de Xamarin Essentials o el webAuthenticator 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
    };
}

Los UserId y DisplayName no están disponibles directamente al usar la autenticación de Azure App Service. En su lugar, use un solicitante diferido para recuperar la información del punto de conexión de /.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);
    }
}

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 clase PurgeOptions 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 a la espera de enviarse al servidor.
  • QueryId especifica un identificador de consulta que se usa para identificar el token delta que se va a 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 no se debe actualizar el token delta.
    • TimestampUpdatePolicy.UpdateToLastEntity indica que el token delta debe actualizarse al campo updatedAt 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 valor de QueryId que usó al llamar a table.PullItemsAsync() para sincronizar los datos. El 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, es posible que tenga que 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 un 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 eventos client.SynchronizationProgress. 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 de args son null o -1 cuando la propiedad no es relevante para el evento de sincronización.