Como usar a biblioteca de cliente de Aplicativos Móveis do Azure para .NET

Este guia mostra como executar cenários comuns usando a biblioteca de cliente .NET para Aplicativos Móveis do Azure. Use a biblioteca de cliente .NET em qualquer aplicativo .NET 6 ou .NET Standard 2.0, incluindo MAUI, Xamarin e Windows (WPF, UWP e WinUI).

Se você é novo nos Aplicativos Móveis do Azure, considere primeiro concluir um dos tutoriais de início rápido:

Nota

Este artigo aborda a edição mais recente (v6.0) do Microsoft Datasync Framework. Para clientes mais antigos, consulte a documentação v4.2.0.

Plataformas suportadas

A biblioteca de cliente .NET suporta qualquer plataforma .NET Standard 2.0 ou .NET 6, incluindo:

  • .NET MAUI para plataformas Android, iOS e Windows.
  • Android API nível 21 e posterior (Xamarin e Android para .NET).
  • iOS versão 12.0 e posterior (Xamarin e iOS para .NET).
  • A Plataforma Universal do Windows compila 19041 e posterior.
  • Estrutura de apresentação do Windows (WPF).
  • SDK de aplicativos do Windows (WinUI 3).
  • Xamarin.Forms

Além disso, amostras foram criadas para Avalonia e Uno Platform. O exemplo TodoApp contém um exemplo de cada plataforma testada.

Configuração e pré-requisitos

Adicione as seguintes bibliotecas do NuGet:

Se estiver usando um projeto de plataforma (por exemplo, MAUI do .NET), certifique-se de adicionar as bibliotecas ao projeto de plataforma e a qualquer projeto compartilhado.

Criar o cliente de serviço

O código a seguir cria o cliente de serviço, que é usado para coordenar toda a comunicação com as tabelas de back-end e offline.

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

No código anterior, substitua MOBILE_APP_URL pela URL do back-end ASP.NET Core. O cliente deve ser criado como um singleton. Se estiver usando um provedor de autenticação, ele pode ser configurado da seguinte forma:

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

Mais detalhes sobre o provedor de autenticação são fornecidos posteriormente neste documento.

Opções

Um conjunto completo (padrão) de opções pode ser criado assim:

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, uma solicitação HTTP é feita passando a solicitação pelo provedor de autenticação (que adiciona o cabeçalho para o Authorization usuário autenticado no momento) antes de enviar a solicitação. Você pode, opcionalmente, adicionar mais manipuladores delegadores. Cada solicitação passa pelos manipuladores delegados antes de ser enviada para o serviço. A delegação de manipuladores permite que você adicione cabeçalhos extras, faça tentativas ou forneça recursos de registro.

Exemplos de manipuladores de delegação são fornecidos para registrar e adicionar cabeçalhos de solicitação mais adiante neste artigo.

IdGenerator

Quando uma entidade é adicionada a uma tabela offline, ela deve ter uma ID. Um ID é gerado se não for fornecido. A IdGenerator opção permite que você personalize o ID que é gerado. Por padrão, um ID globalmente exclusivo é gerado. Por exemplo, a configuração a seguir gera uma cadeia de caracteres que inclui o nome da tabela e um GUID:

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

InstallationId

Se um for definido, um cabeçalho X-ZUMO-INSTALLATION-ID personalizado será enviado com cada solicitação para identificar a combinação do aplicativo em um InstallationId dispositivo específico. Esse cabeçalho pode ser gravado em logs e permite determinar o número de instalações distintas para seu aplicativo. Se você usar InstallationIdo , o ID deve ser armazenado em armazenamento persistente no dispositivo para que instalações exclusivas possam ser rastreadas.

Loja Offline

O OfflineStore é usado ao configurar o acesso a dados offline. Para obter mais informações, consulte Trabalhar com tabelas offline.

Operações paralelas

Parte do processo de sincronização offline envolve o envio de operações em fila para o servidor remoto. Quando a operação push é acionada, as operações são enviadas na ordem em que foram recebidas. Você pode, opcionalmente, usar até oito threads para enviar essas operações. As operações paralelas usam mais recursos no cliente e no servidor para concluir a operação mais rapidamente. A ordem em que as operações chegam ao servidor não pode ser garantida ao usar vários threads.

SerializerSettings

Se você alterou as configurações do serializador no servidor de sincronização de dados, precisará fazer as mesmas alterações no SerializerSettings cliente. Essa opção permite que você especifique suas próprias configurações do serializador.

TableEndpointResolver

Por convenção, as tabelas estão localizadas no serviço remoto no caminho (conforme especificado pelo Route atributo no /tables/{tableName} código do servidor). No entanto, as tabelas podem existir em qualquer caminho de ponto de extremidade. O TableEndpointResolver é uma função que transforma um nome de tabela em um caminho para comunicação com o serviço remoto.

Por exemplo, o seguinte altera a suposição para que todas as tabelas estejam localizadas em /api:

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

UserAgent

O cliente de sincronização de dados gera um valor de cabeçalho User-Agent adequado com base na versão da biblioteca. Alguns desenvolvedores acham que o cabeçalho do agente do usuário vaza informações sobre o cliente. Você pode definir a UserAgent propriedade como qualquer valor de cabeçalho válido.

Trabalhar com tabelas remotas

A seção a seguir detalha como pesquisar e recuperar registros e modificar os dados em uma tabela remota. São abordados os seguintes tópicos:

Criar uma referência de tabela remota

Para criar uma referência de tabela remota, use GetRemoteTable<T>:

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

Se desejar retornar uma tabela somente leitura, use a IReadOnlyRemoteTable<T> versão:

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

O tipo de modelo deve implementar o ITableData contrato do serviço. Use DatasyncClientData para fornecer os campos obrigatórios:

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

O DatasyncClientData objeto inclui:

  • Id (string) - um ID globalmente exclusivo para o item.
  • UpdatedAt (System.DataTimeOffset) - a data/hora em que o item foi atualizado pela última vez.
  • Version (string) - uma cadeia de caracteres opaca usada para versionamento.
  • Deleted (booleano) - se true, o item é excluído.

O serviço mantém esses campos. Não ajuste esses campos como parte do seu aplicativo cliente.

Os modelos podem ser anotados usando atributos Newtonsoft.JSON. O nome da tabela pode ser especificado usando o DataTable atributo:

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

Como alternativa, especifique o nome da tabela na GetRemoteTable() chamada:

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

O cliente usa o caminho /tables/{tablename} como o URI. O nome da tabela também é o nome da tabela offline no banco de dados SQLite.

Tipos suportados

Além dos tipos primitivos (int, float, string, etc.), os seguintes tipos são suportados para modelos:

  • System.DateTime - como uma cadeia de data/hora ISO-8601 UTC com precisão ms.
  • System.DateTimeOffset - como uma cadeia de data/hora ISO-8601 UTC com precisão ms.
  • System.Guid - formatado como 32 dígitos separados como hífenes.

Consultar dados de um servidor remoto

A tabela remota pode ser usada com instruções do tipo LINQ, incluindo:

  • Filtragem com uma .Where() cláusula.
  • Classificação com várias .OrderBy() cláusulas.
  • Seleção de propriedades com .Select().
  • Paginação com .Skip() e .Take().

Contar itens de uma consulta

Se precisar de uma contagem dos itens que a consulta retornaria, você pode usar .CountItemsAsync() em uma tabela ou .LongCountAsync() em uma 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();

Esse método causa uma viagem de ida e volta para o servidor. Você também pode obter uma contagem enquanto preenche uma lista (por exemplo), evitando a viagem extra de ida e volta:

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

A contagem será preenchida após a primeira solicitação para recuperar o conteúdo da tabela.

Retornando todos os dados

Os dados são retornados por meio de um IAsyncEnumerable:

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

Use qualquer uma das seguintes cláusulas de encerramento para converter a IAsyncEnumerable<T> coleção em uma coleção 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();

Nos bastidores, a tabela remota lida com a paginação do resultado para você. Todos os itens são retornados independentemente de quantas solicitações do lado do servidor são necessárias para atender à consulta. Esses elementos também estão disponíveis nos resultados da consulta (por exemplo, remoteTable.Where(m => m.Rating == "R")).

A estrutura de sincronização de dados também fornece ConcurrentObservableCollection<T> - uma coleção observável thread-safe. Essa classe pode ser usada no contexto de aplicativos de interface do usuário que normalmente seriam usados ObservableCollection<T> para gerenciar uma lista (por exemplo, Xamarin Forms ou listas MAUI). Você pode limpar e carregar um ConcurrentObservableCollection<T> diretamente de uma tabela ou consulta:

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

O uso .ToObservableCollection(collection) aciona o CollectionChanged evento uma vez para toda a coleção em vez de para itens individuais, resultando em um tempo de redesenho mais rápido.

O ConcurrentObservableCollection<T> também tem modificações orientadas 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);

Modificações controladas por predicados podem ser usadas em manipuladores de eventos quando o índice do item não é conhecido antecipadamente.

Filtrar dados

Você pode usar uma .Where() cláusula para filtrar dados. Por exemplo:

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

A filtragem é feita no serviço antes do IAsyncEnumerable e no cliente após o IAsyncEnumerable. Por exemplo:

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

A primeira .Where() cláusula (retornar apenas itens incompletos) é executada no serviço, enquanto a segunda .Where() cláusula (começando com "O") é executada no cliente.

A Where cláusula suporta operações que são traduzidas para o subconjunto OData. As operações incluem:

  • Operadores relacionais (==, , , , <>!=<=, ), >=
  • Operadores aritméticos (+, , , -/*, %),
  • Precisão numérica (Math.Floor, Math.Ceiling),
  • Funções string (, , , , , StartsWithSubstring, EqualsReplaceEndsWithIndexOf) (Lengthapenas culturas ordinais e invariantes),
  • Propriedades de data (Year, , , , SecondHourMinute, MonthDay),
  • Acessar propriedades de um objeto e
  • Expressões que combinam qualquer uma dessas operações.

Ordenar dados

Use .OrderBy(), , .ThenBy().OrderByDescending()e .ThenByDescending() com um acessador de propriedade para classificar dados.

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

A triagem é feita pelo serviço. Não é possível especificar uma expressão em nenhuma cláusula de classificação. Se desejar classificar por uma expressão, use a classificação do lado do cliente:

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

Seleção de propriedades

Você pode retornar um subconjunto de dados do serviço:

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

Retornar uma página de dados

Você pode retornar um subconjunto do conjunto de dados usando .Skip() e .Take() para implementar a paginação:

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

Em um aplicativo do mundo real, você pode usar consultas semelhantes ao exemplo anterior com um controle de pager ou uma interface do usuário comparável para navegar entre páginas.

Todas as funções descritas até agora são aditivas, para que possamos continuar a encadeá-las. Cada chamada encadeada afeta mais da consulta. Mais um exemplo:

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

Procurar dados remotos por ID

A GetItemAsync função pode ser usada para procurar objetos do banco de dados com um ID específico.

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

Se o item que você está tentando recuperar tiver sido excluído suavemente, você deverá usar o 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);

Inserir dados no servidor remoto

Todos os tipos de cliente devem conter um membro chamado Id, que é, por padrão, uma cadeia de caracteres. Essa ID é necessária para executar operações CRUD e para sincronização offline. O código a seguir ilustra como usar o InsertItemAsync método para inserir novas linhas em uma tabela. O parâmetro contém os dados a serem inseridos como um objeto .NET.

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

Se um valor de ID personalizado exclusivo não for incluído durante uma item inserção, o servidor gerará um ID. Você pode recuperar a ID gerada inspecionando o objeto após o retorno da chamada.

Atualizar dados no servidor remoto

O código a seguir ilustra como usar o ReplaceItemAsync método para atualizar um registro existente com a mesma ID com novas informações.

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

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

Excluir dados no servidor remoto

O código a seguir ilustra como usar o DeleteItemAsync método para excluir uma instância existente.

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

await todoTable.DeleteItemAsync(item);

Resolução de conflitos e simultaneidade otimista

Dois ou mais clientes podem gravar alterações no mesmo item ao mesmo tempo. Sem a deteção de conflitos, a última gravação substituiria quaisquer atualizações anteriores. O controle de simultaneidade otimista pressupõe que cada transação pode ser confirmada e, portanto, não usa nenhum bloqueio de recursos. O controle de simultaneidade otimista verifica se nenhuma outra transação modificou os dados antes de confirmá-los. Se os dados tiverem sido modificados, a transação será revertida.

Os Aplicativos Móveis do Azure dão suporte ao controle de simultaneidade otimista controlando as alterações em cada item usando a coluna de propriedades do sistema definida para cada tabela no back-end do version Aplicativo Móvel. Sempre que um registo é atualizado, as Aplicações Móveis definem a version propriedade desse registo para um novo valor. Durante cada solicitação de atualização, a propriedade do registro incluído com a solicitação é comparada com a version mesma propriedade para o registro no servidor. Se a versão passada com a solicitação não corresponder ao back-end, a biblioteca do cliente gerará uma DatasyncConflictException<T> exceção. O tipo incluído com a exceção é o registro do back-end que contém a versão do servidor do registro. O aplicativo pode usar essas informações para decidir se deseja executar a solicitação de atualização novamente com o valor correto version do back-end para confirmar alterações.

A simultaneidade otimista é ativada automaticamente ao usar o DatasyncClientData objeto base.

Além de habilitar a simultaneidade otimista, você também deve capturar a DatasyncConflictException<T> exceção em seu código. Resolva o conflito aplicando o correto version ao registro atualizado e, em seguida, repita a chamada com o registro resolvido. O código a seguir mostra como resolver um conflito de gravação uma vez detetado:

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

Trabalhar com tabelas offline

As tabelas offline usam um repositório SQLite local para armazenar dados para uso quando offline. Todas as operações de tabela são feitas no repositório SQLite local em vez do armazenamento do servidor remoto. Certifique-se de adicionar o Microsoft.Datasync.Client.SQLiteStore a cada projeto de plataforma e a quaisquer projetos compartilhados.

Antes que uma referência de tabela possa ser criada, o armazenamento local deve ser preparado:

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

Uma vez que a loja tenha sido definida, você pode criar o cliente:

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

Finalmente, você deve garantir que os recursos offline sejam inicializados:

await client.InitializeOfflineStoreAsync();

A inicialização da loja normalmente é feita imediatamente após a criação do cliente. O OfflineConnectionString é um URI usado para especificar o local do banco de dados SQLite e as opções usadas para abrir o banco de dados. Para obter mais informações, consulte Nomes de arquivos URI no SQLite.

  • Para usar um cache na memória, use file:inmemory.db?mode=memory&cache=private.
  • Para usar um arquivo, use file:/path/to/file.db

Você deve especificar o nome de arquivo absoluto para o arquivo. Se estiver usando o Xamarin, você poderá usar os Auxiliares do Sistema de Arquivos do Xamarin Essentials para construir um caminho: Por exemplo:

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

Se você estiver usando MAUI, você pode usar o MAUI File System Helpers para construir um caminho: Por exemplo:

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

Criar uma tabela offline

Uma referência de tabela pode ser obtida usando o GetOfflineTable<T> método:

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

Assim como na tabela remota, você também pode expor uma tabela offline somente leitura:

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

Não é necessário autenticar para usar uma tabela offline. Você só precisa autenticar quando estiver se comunicando com o serviço de back-end.

Sincronizar uma tabela offline

As tabelas offline não são sincronizadas com o back-end por padrão. A sincronização é dividida em duas partes. Você pode enviar as alterações por push separadamente do download de novos itens. Por exemplo:

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

Por padrão, todas as tabelas usam sincronização incremental - apenas novos registros são recuperados. Um registro é incluído para cada consulta exclusiva (gerada pela criação de um hash MD5 da consulta OData).

Nota

O primeiro argumento é PullItemsAsync a consulta OData que indica quais registros devem ser puxados para o dispositivo. É melhor modificar o serviço para retornar apenas registros específicos para o usuário do que criar consultas complexas no lado do cliente.

As opções (definidas pelo PullOptions objeto) geralmente não precisam ser definidas. As opções incluem:

  • PushOtherTables - Se definido como true, todas as tabelas são empurradas.
  • QueryId - um ID de consulta específico para usar em vez do gerado.
  • WriteDeltaTokenInterval - com que frequência escrever o delta-token usado para rastrear a sincronização incremental.

O SDK executa um implícito PushAsync() antes de extrair registros.

O tratamento de conflitos acontece em um PullAsync() método. Lide com conflitos da mesma forma que as tabelas online. O conflito é produzido quando é chamado em PullAsync() vez de durante a inserção, atualização ou exclusão. Se vários conflitos acontecerem, eles serão agrupados em um único PushFailedException. Lide com cada falha separadamente.

Enviar alterações por push para todas as tabelas

Para enviar todas as alterações para o servidor remoto, use:

await client.PushTablesAsync();

Para enviar por push alterações para um subconjunto de tabelas, forneça um IEnumerable<string> para o PushTablesAsync() método:

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

Use a client.PendingOperations propriedade para ler o número de operações aguardando para serem enviadas por push para o serviço remoto. Esta propriedade ocorre null quando nenhum repositório offline foi configurado.

Executar consultas SQLite complexas

Se você precisar fazer consultas SQL complexas no banco de dados offline, poderá fazê-lo usando o ExecuteQueryAsync() método. Por exemplo, para fazer uma SQL JOIN instrução, defina um JObject que mostre a estrutura do valor de retorno e, em seguida, 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.

A definição é um conjunto de chaves/valores. As chaves devem corresponder aos nomes de campo que a consulta SQL retorna e os valores devem ser o valor padrão do tipo esperado. Use 0L para números (longos), false para booleanos e string.Empty para tudo o mais.

SQLite tem um conjunto restritivo de tipos suportados. Data/horas são armazenadas como o número de milissegundos desde a época para permitir comparações.

Autenticar utilizadores

Os Aplicativos Móveis do Azure permitem gerar um provedor de autenticação para lidar com chamadas de autenticação. Especifique o provedor de autenticação ao construir o cliente de serviço:

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

Sempre que a autenticação é necessária, o provedor de autenticação é chamado para obter o token. Um provedor de autenticação genérico pode ser usado para autenticação baseada em cabeçalho de autorização e autenticação baseada em Autorização e Autenticação do Serviço de Aplicativo. Use o seguinte 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 */"
    };
}

Os tokens de autenticação são armazenados em cache na memória (nunca gravados no dispositivo) e atualizados quando necessário.

Usar a plataforma de identidade da Microsoft

A plataforma de identidade da Microsoft permite que você se integre facilmente ao Microsoft Entra ID. Consulte os tutoriais de início rápido para obter um tutorial completo sobre como implementar a autenticação do Microsoft Entra. O código a seguir mostra um exemplo de recuperação do token de acesso:

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 obter mais informações sobre como integrar a plataforma de identidade da Microsoft com o ASP.NET 6, consulte a documentação da plataforma de identidade da Microsoft.

Usar o Xamarin Essentials ou o MAUI WebAuthenticator

Para a Autenticação do Serviço de Aplicativo do Azure, você pode usar o Xamarin Essentials WebAuthenticator ou o MAUI WebAuthenticator para obter um 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
    };
}

O e DisplayName não estão diretamente disponíveis ao usar a UserId Autenticação do Serviço de Aplicativo do Azure. Em vez disso, use um solicitante preguiçoso para recuperar as informações do /.auth/me ponto de extremidade:

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

Tópicos avançados

Limpando entidades no banco de dados local

Em operação normal, a limpeza de entidades não é necessária. O processo de sincronização remove entidades excluídas e mantém os metadados necessários para tabelas de banco de dados locais. No entanto, há momentos em que limpar entidades dentro do banco de dados é útil. Um desses cenários é quando você precisa excluir um grande número de entidades e é mais eficiente limpar dados da tabela localmente.

Para limpar registros de uma tabela, use table.PurgeItemsAsync():

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

A consulta identifica as entidades a serem removidas da tabela. Identifique as entidades a serem limpas usando o LINQ:

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

A PurgeOptions classe fornece configurações para modificar a operação de limpeza:

  • DiscardPendingOperations Descarta todas as operações pendentes para a tabela que estão na fila de operações aguardando para serem enviadas ao servidor.
  • QueryId especifica uma ID de consulta que é usada para identificar o token delta a ser usado para a operação.
  • TimestampUpdatePolicy Especifica como ajustar o token delta no final da operação de limpeza:
    • TimestampUpdatePolicy.NoUpdate indica que o token delta não deve ser atualizado.
    • TimestampUpdatePolicy.UpdateToLastEntity indica que o token delta deve ser atualizado para o updatedAt campo da última entidade armazenada na tabela.
    • TimestampUpdatePolicy.UpdateToNow indica que o token delta deve ser atualizado para a data/hora atual.
    • TimestampUpdatePolicy.UpdateToEpoch indica que o token delta deve ser redefinido para sincronizar todos os dados.

Use o mesmo QueryId valor usado ao ligar table.PullItemsAsync() para sincronizar dados. O QueryId especifica o token delta a ser atualizado quando a limpeza for concluída.

Personalizar cabeçalhos de solicitação

Para dar suporte ao cenário específico do aplicativo, talvez seja necessário personalizar a comunicação com o back-end do aplicativo móvel. Por exemplo, você pode adicionar um cabeçalho personalizado a cada solicitação de saída ou alterar os códigos de status de resposta antes de retornar ao usuário. Use um DelegatingHandler personalizado, como no exemplo a seguir:

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

Ativar o registo de pedidos

Você também pode usar um DelegatingHandler para adicionar log de solicitação:

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

Monitorar eventos de sincronização

Quando um evento de sincronização acontece, o evento é publicado para o delegado do client.SynchronizationProgress evento. Os eventos podem ser usados para monitorar o progresso do processo de sincronização. Defina um manipulador de eventos de sincronização da seguinte maneira:

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

O SynchronizationEventArgs tipo é definido da seguinte forma:

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

As propriedades dentro args são ou null-1 quando a propriedade não é relevante para o evento de sincronização.