Como usar a biblioteca de cliente de Aplicativos Móveis do Azure para .NET
Observação
Este produto foi retirado. Para obter uma substituição para projetos que usam o .NET 8 ou posterior, consulte a biblioteca Community Toolkit Datasync.
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:
- AvaloniaUI
- MAUI (Android e iOS)
- Plataforma Uno
- Windows (UWP)
- Windows (WinUI3)
- Windows (WPF)
- Xamarin (Android Nativo)
- Xamarin (iOS nativo)
- Xamarin Forms (Android e iOS)
Observação
Este artigo aborda a edição mais recente (v6.0) do Microsoft Datasync Framework. Para clientes mais antigos, consulte a documentação do 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.Formulários
Além disso, amostras foram criadas para Avalonia e Uno Platform. O de exemplo
Configuração e pré-requisitos
Adicione as seguintes bibliotecas do NuGet:
- Microsoft.Datasync.Client
- Microsoft.Datasync.Client.SQLiteStore se estiver usando tabelas offline.
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 Authorization
para o 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 de log e adição de 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 opção IdGenerator
permite personalizar o ID 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 uma InstallationId
for definida, uma X-ZUMO-INSTALLATION-ID
de cabeçalho personalizada será enviada com cada solicitação para identificar a combinação do aplicativo em um 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 InstallationId
, 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
no 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 /tables/{tableName}
(conforme especificado pelo atributo Route
no 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 de 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 propriedade UserAgent
para 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 de referência de tabela
- Consultar dados
- Contar itens de uma consulta
- Procurar dados remotos por ID
- Inserir dados no servidor remoto
- Atualizar dados no servidor remoto
- Excluir dados no servidor remoto
- Resolução de conflitos e simultaneidade otimista
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 versão IReadOnlyRemoteTable<T>
:
IReadOnlyRemoteTable<TodoItem> remoteTable = client.GetRemoteTable();
O tipo de modelo deve implementar o contrato de ITableData
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 objeto DatasyncClientData
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) - setrue
, 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 atributo DataTable
:
[DataTable("todoitem")]
public class MyTodoItemClass : DatasyncClientData
{
public string Title { get; set; }
public bool IsComplete { get; set; }
}
Como alternativa, especifique o nome da tabela na chamada GetRemoteTable()
:
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 UTC ISO-8601 com precisão ms. -
System.DateTimeOffset
- como uma cadeia de data/hora UTC ISO-8601 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 cláusula
.Where()
. - Classificação com várias cláusulas
.OrderBy()
. - 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 o IAsyncEnumerable<T>
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 segura para threads. Essa classe pode ser usada no contexto de aplicativos de interface do usuário que normalmente usariam 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 do .ToObservableCollection(collection)
aciona o evento CollectionChanged
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.
Filtragem de dados
Você pode usar uma cláusula .Where()
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 cláusula .Where()
(devolver apenas itens incompletos) é executada no serviço, enquanto a segunda cláusula .Where()
(começando com "O") é executada no cliente.
A cláusula Where
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 (
Length
,Substring
,Replace
,IndexOf
,Equals
,StartsWith
,EndsWith
) (apenas culturas ordinais e invariantes), - Propriedades de data (
Year
,Month
,Day
,Hour
,Minute
,Second
), - Acessar propriedades de um objeto e
- Expressões que combinam qualquer uma dessas operações.
Ordenar dados
Use .OrderBy()
, .OrderByDescending()
, .ThenBy()
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 função GetItemAsync
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 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);
Inserir dados no servidor remoto
Todos os tipos de cliente devem conter um membro chamado Id, que é, por padrão, uma cadeia de caracteres. Este Id é necessário para executar operações CRUD e para sincronização offline. O código a seguir ilustra como usar o método InsertItemAsync
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 no item
durante uma 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 método ReplaceItemAsync
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 método DeleteItemAsync
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. 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 propriedade do sistema version
definida para cada tabela no back-end do Aplicativo Móvel. Cada vez que um registro é atualizado, os Aplicativos Móveis definem a propriedade version
desse registro como um novo valor. Durante cada solicitação de atualização, a propriedade version
do registro incluído com a solicitação é comparada à mesma propriedade do registro no servidor. Se a versão passada com a solicitação não corresponder ao back-end, a biblioteca do cliente gerará uma exceção DatasyncConflictException<T>
. 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 de version
correto do back-end para confirmar alterações.
A simultaneidade otimista é ativada automaticamente ao usar o objeto base DatasyncClientData
.
Além de habilitar a simultaneidade otimista, você também deve capturar a exceção DatasyncConflictException<T>
em seu código. Resolva o conflito aplicando o version
correto ao registro atualizado e 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
- 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 o 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, poderá usar o Auxiliares do Sistema de Arquivos MAUI 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 método GetOfflineTable<T>
:
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).
Observação
O primeiro argumento a 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 objeto PullOptions
) geralmente não precisam ser definidas. As opções incluem:
-
PushOtherTables
- se definido como true, todas as tabelas são enviadas por push. -
QueryId
- um ID de consulta específico para usar em vez do gerado. -
WriteDeltaTokenInterval
- com que frequência escrever o token delta usado para controlar a sincronização incremental.
O SDK executa uma PushAsync()
implícita antes de extrair registros.
O tratamento de conflitos acontece de acordo com um método PullAsync()
. Lide com conflitos da mesma forma que as tabelas online. O conflito é produzido quando PullAsync()
é chamado em 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 uma IEnumerable<string>
para o método PushTablesAsync()
:
var tablesToPush = new string[] { "TodoItem", "Notes" };
await client.PushTables(tablesToPush);
Use a propriedade client.PendingOperations
para ler o número de operações aguardando para serem enviadas por push para o serviço remoto. Essa propriedade é null
quando nenhum armazenamento offline foi configurado.
Executar consultas SQLite complexas
Se você precisar fazer consultas SQL complexas no banco de dados offline, poderá fazê-lo usando o método ExecuteQueryAsync()
. Por exemplo, para fazer uma instrução SQL JOIN
, 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 todo o resto.
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 usuários
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 a integração da plataforma de identidade da Microsoft com o ASP.NET 6, consulte a documentação 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
};
}
Os UserId
e DisplayName
não estão diretamente disponíveis ao usar a Autenticação do Serviço de Aplicativo do Azure. Em vez disso, use um solicitante preguiçoso para recuperar as informações do ponto de extremidade /.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);
}
}
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 classe PurgeOptions
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 campoupdatedAt
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 valor de QueryId
usado ao chamar 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 personalizado DelegatingHandler, 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 client.SynchronizationProgress
delegado de eventos. 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 tipo SynchronizationEventArgs
é 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 null
ou -1
quando a propriedade não é relevante para o evento de sincronização.