Como usar a biblioteca de clientes dos Aplicativos Móveis do Azure para .NET
Nota
Este produto está desativado. Para obter uma substituição para projetos que usam o .NET 8 ou posterior, consulte a biblioteca datasync do Kit de Ferramentas da Comunidade .
Este guia mostra como executar cenários comuns usando a biblioteca de clientes do .NET para Aplicativos Móveis do Azure. Use a biblioteca de clientes .NET em qualquer aplicativo .NET 6 ou .NET Standard 2.0, incluindo MAUI, Xamarin e Windows (WPF, UWP e WinUI).
Se você não estiver familiarizado com os Aplicativos Móveis do Azure, considere primeiro concluir um dos tutoriais de início rápido:
- AvaloniaUI
- MAUI (Android e iOS)
- da Plataforma Uno
- windows (UWP)
- Windows (WinUI3)
- do WPF (
Windows) - Xamarin (Android Native)
- Xamarin (nativo do iOS)
- do Xamarin Forms (Android e iOS)
Nota
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 com suporte
A biblioteca de clientes do .NET dá suporte a qualquer plataforma .NET Standard 2.0 ou .NET 6, incluindo:
- MAUI do .NET para plataformas Android, iOS e Windows.
- API do Android 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 do Aplicativo do Windows (WinUI 3).
- Xamarin.Forms
Além disso, foram criados exemplos para Avalonia e da Plataforma Uno. O exemplo TodoApp contém um exemplo de cada plataforma testada.
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, .NET MAUI), certifique-se de adicionar as bibliotecas ao projeto da 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
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 da seguinte maneira:
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 por meio do provedor de autenticação (que adiciona o cabeçalho Authorization
para o usuário autenticado no momento) antes de enviar a solicitação. Opcionalmente, você pode adicionar mais manipuladores de delegação. Cada solicitação passa pelos manipuladores de delegação antes de ser enviada ao serviço. Delegar manipuladores permite que você adicione cabeçalhos extras, tente novamente ou forneça recursos de log.
Exemplos de delegação de manipuladores são fornecidos para de log e adicionar cabeçalhos de solicitação posteriormente neste artigo.
IdGenerator
Quando uma entidade é adicionada a uma tabela offline, ela deve ter uma ID. Uma ID será gerada se uma não for fornecida. A opção IdGenerator
permite que você adapte a ID gerada. Por padrão, uma ID global exclusiva é gerada. Por exemplo, a seguinte configuração 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 InstallationId
for definido, um cabeçalho personalizado X-ZUMO-INSTALLATION-ID
será enviado com cada solicitação para identificar a combinação do aplicativo em um dispositivo específico. Esse cabeçalho pode ser registrado em logs e permite determinar o número de instalações distintas para seu aplicativo. Se você usar InstallationId
, a ID deverá ser armazenada no armazenamento persistente no dispositivo para que as instalações exclusivas possam ser controladas.
OfflineStore
O OfflineStore
é usado ao configurar o acesso a dados offline. Para obter mais informações, consulte Trabalhar com tabelas offline.
ParallelOperations
Parte do processo de sincronização offline envolve enviar por push operações enfileiradas para o servidor remoto. Quando a operação de push é disparada, as operações são enviadas na ordem em que foram recebidas. Opcionalmente, você pode usar até oito threads para efetuar push dessas operações. As operações paralelas usam mais recursos no cliente e no servidor para concluir a operação mais rapidamente. A ordem na qual 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 de 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 se comunicar 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 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. Os seguintes tópicos são abordados:
- Criar uma referência de tabela
- de dados de consulta
- Contar itens de um de consulta
- pesquisar dados remotos por ID
- Inserir dados no servidor remoto
- Atualizar dados no servidor remoto
- Excluir dados no servidor remoto
- resolução de conflitos e de 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 você quiser 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 necessários:
public class TodoItem : DatasyncClientData
{
public string Title { get; set; }
public bool IsComplete { get; set; }
}
O objeto DatasyncClientData
inclui:
-
Id
(cadeia de caracteres) – uma ID global exclusiva para o item. -
UpdatedAt
(System.DataTimeOffset) – a data/hora em que o item foi atualizado pela última vez. -
Version
(cadeia de caracteres) – uma cadeia de caracteres opaca usada para controle de versão. -
Deleted
(booliano) – setrue
, o item será excluído.
O serviço mantém esses campos. Não ajuste esses campos como parte do 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 com suporte
Além dos tipos primitivos (int, float, string etc.), os seguintes tipos têm suporte para modelos:
-
System.DateTime
- como uma cadeia de caracteres de data/hora UTC ISO-8601 com precisão ms. -
System.DateTimeOffset
- como uma cadeia de caracteres de data/hora UTC ISO-8601 com precisão ms. -
System.Guid
- formatado como 32 dígitos separados como hifens.
Consultar dados de um servidor remoto
A tabela remota pode ser usada com instruções semelhantes a LINQ, incluindo:
- Filtragem com uma cláusula
.Where()
. - Classificação com várias cláusulas
.OrderBy()
. - Selecionando propriedades com
.Select()
. - Paginação com
.Skip()
e.Take()
.
Contar itens de uma consulta
Se você precisar de uma contagem dos itens que a consulta retornaria, poderá 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 ao preencher uma lista (por exemplo), evitando a viagem de ida e volta extra:
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 umIAsyncEnumerable
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 manipula 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 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);
Usar .ToObservableCollection(collection)
dispara o evento CollectionChanged
uma vez para toda a coleção, em vez de para itens individuais, resultando em um tempo de redraw mais rápido.
O ConcurrentObservableCollection<T>
também tem modificações controladas por predicado:
// 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 predicado podem ser usadas em manipuladores de eventos quando o índice do item não é conhecido com antecedência.
Filtrando 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()
(retornar somente itens incompletos) é executada no serviço, enquanto a segunda cláusula .Where()
(começando com "The") é executada no cliente.
A cláusula Where
dá suporte a operações que serão convertidas no subconjunto OData. As operações incluem:
- Operadores relacionais (
==
,!=
,<
,<=
,>
,>=
), - Operadores aritméticos (
+
,-
,/
,*
,%
), - Precisão de número (
Math.Floor
,Math.Ceiling
), - Funções de cadeia de caracteres (
Length
,Substring
,Replace
,IndexOf
,Equals
,StartsWith
,EndsWith
) (somente culturas ordinais e invariáveis), - Propriedades de data (
Year
,Month
,Day
,Hour
,Minute
,Second
), - Acessar propriedades de um objeto e
- Expressões que combinam qualquer uma dessas operações.
Classificando 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 classificação é feita pelo serviço. Você não pode especificar uma expressão em nenhuma cláusula de classificação. Se você quiser classificar por uma expressão, use a classificação do lado do cliente:
var items = await remoteTable.ToListAsync().OrderBy(x => x.Title.ToLowerCase());
Selecionando 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 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 encadeando-as. 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();
Pesquisar dados remotos por ID
A função GetItemAsync
pode ser usada para pesquisar objetos do banco de dados com uma ID específica.
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. Esse de 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 estiver incluído no item
durante uma inserção, o servidor gerará uma 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 detecção de conflitos, a última gravação substituiria as 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 confirmar os dados. 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 acompanhando 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. Sempre que um registro é atualizado, os Aplicativos Móveis definem a propriedade version
para esse 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 de clientes 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 de servidores do registro. Em seguida, o aplicativo pode usar essas informações para decidir se executará a solicitação de atualização novamente com o valor version
correto do back-end para confirmar as alterações.
A simultaneidade otimista é habilitada 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 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();
}
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 repositório de servidor remoto. Adicione 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 repositório local deve estar preparado:
var store = new OfflineSQLiteStore(Constants.OfflineConnectionString);
store.DefineTable<TodoItem>();
Depois que o repositório tiver sido definido, você poderá criar o cliente:
var options = new DatasyncClientOptions
{
OfflineStore = store
};
var client = new DatasyncClient("MOBILE_URL", options);
Por fim, você deve garantir que os recursos offline sejam inicializados:
await client.InitializeOfflineStoreAsync();
A inicialização do repositório 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 arquivo 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 o MAUI, poderá usar os 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 acontece com a tabela remota, você também pode expor uma tabela offline somente leitura:
IReadOnlyOfflineTable<TodoItem> table = client.GetOfflineTable<TodoItem>();
Você não precisa se autenticar para usar uma tabela offline. Você só precisa se 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 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 – somente 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 a PullItemsAsync
é a consulta OData que indica quais registros efetuar pull para o dispositivo. É melhor modificar o serviço para retornar apenas registros específicos ao usuário em vez de 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
- uma ID de consulta específica a ser usada em vez da gerada. -
WriteDeltaTokenInterval
- com que frequência gravar o token delta usado para acompanhar a sincronização incremental.
O SDK executa um PushAsync()
implícito antes de efetuar pull de registros.
O tratamento de conflitos ocorre em um método PullAsync()
. Lidar 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 ocorrerem vários conflitos, eles serão agrupados em um único PushFailedException
. Manipule cada falha separadamente.
Enviar alterações por push para todas as tabelas
Para enviar todas as alterações por push para o servidor remoto, use:
await client.PushTablesAsync();
Para enviar alterações por push para um subconjunto de tabelas, forneça uma IEnumerable<string>
ao 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 que estão esperando para serem enviadas por push para o serviço remoto. Essa propriedade é 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á fazer isso usando o método ExecuteQueryAsync()
. Por exemplo, para fazer uma instrução SQL JOIN
, defina uma JObject
que mostre a estrutura do valor retornado e 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 chave/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 boolianos e string.Empty
para todo o resto.
O SQLite tem um conjunto restritivo de tipos com suporte. Data/hora 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 que você gere 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érica 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 à ID do Microsoft Entra. Confira 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 ao 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 webAuthenticator do Xamarin Essentials ou o webAuthenticator maui 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 lento 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
Limpar 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 a limpeza de entidades no banco de dados é útil. Um desses cenários é quando você precisa excluir um grande número de entidades e é mais eficiente apagar 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 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 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 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 de aplicativo específico, 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 códigos de status de resposta antes de retornar ao usuário. Use umDelegatingHandler
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;
}
}
Habilitar registro em log de solicitação
Você também pode usar um DelegatingHandler para adicionar o registro em log de solicitações:
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 no delegado de eventos client.SynchronizationProgress
. 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 de SynchronizationEventArgs
é definido da seguinte maneira:
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.