如何使用适用于 .NET 的 Azure 移动应用客户端库

注意

此产品已停用。 有关使用 .NET 8 或更高版本的项目的替换,请参阅 Community Toolkit Datasync 库

本指南介绍如何使用适用于 Azure 移动应用的 .NET 客户端库执行常见方案。 在任何 .NET 6 或 .NET Standard 2.0 应用程序中使用 .NET 客户端库,包括 MAUI、Xamarin 和 Windows(WPF、UWP 和 WinUI)。

如果你不熟悉 Azure 移动应用,请考虑先完成其中一个快速入门教程:

注意

本文介绍 Microsoft Datasync Framework 的最新版(v6.0)。 对于较旧的客户端,请参阅 v4.2.0 文档

支持的平台

.NET 客户端库支持任何 .NET Standard 2.0 或 .NET 6 平台,包括:

  • 适用于 Android、iOS 和 Windows 平台的 .NET MAUI。
  • Android API 级别 21 及更高版本(Xamarin 和 Android for .NET)。
  • iOS 版本 12.0 及更高版本(适用于 .NET 的 Xamarin 和 iOS)。
  • 通用 Windows 平台版本 19041 及更高版本。
  • Windows Presentation Framework (WPF)。
  • Windows 应用 SDK (WinUI 3)。
  • Xamarin.Forms

此外,还为 AvaloniaUno Platform创建了示例。 TodoApp 示例 包含每个测试平台的示例。

设置和先决条件

从 NuGet 添加以下库:

如果使用平台项目(例如 .NET MAUI),请确保将库添加到平台项目和任何共享项目。

创建服务客户端

以下代码创建服务客户端,用于协调与后端和脱机表的所有通信。

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

在前面的代码中,将 MOBILE_APP_URL 替换为 ASP.NET 核心后端的 URL。 客户端应创建为单一实例。 如果使用身份验证提供程序,可以按如下所示进行配置:

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

本文档后面提供了有关身份验证提供程序的更多详细信息。

选项

可以创建一组完整的(默认)选项,如下所示:

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

通常,HTTP 请求是通过身份验证提供程序(在发送请求之前添加当前经过身份验证的用户的 Authorization 标头)传递请求来发出的。 可以选择性地添加更多委派处理程序。 每个请求在发送到服务之前通过委派处理程序。 委派处理程序允许添加额外的标头、重试或提供日志记录功能。

日志记录 提供了委派处理程序的示例,本文后面部分 添加请求标头。

IdGenerator

将实体添加到脱机表时,它必须具有 ID。 如果未提供 ID,则会生成 ID。 使用 IdGenerator 选项可以定制生成的 ID。 默认情况下,将生成全局唯一 ID。 例如,以下设置生成包含表名称和 GUID 的字符串:

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

InstallationId

如果设置了 InstallationId,则会随每个请求发送自定义标头 X-ZUMO-INSTALLATION-ID,以标识特定设备上的应用程序组合。 此标头可以记录在日志中,并允许你确定应用的不同安装数。 如果使用 InstallationId,ID 应存储在设备上的永久性存储中,以便跟踪唯一安装。

OfflineStore

配置脱机数据访问时,将使用 OfflineStore。 有关详细信息,请参阅 使用脱机表

ParallelOperations

脱机同步过程的一部分涉及将排队操作推送到远程服务器。 触发推送操作时,会按照收到操作的顺序提交操作。 可以选择使用最多八个线程来推送这些操作。 并行操作在客户端和服务器上使用更多资源来更快地完成操作。 使用多个线程时无法保证操作到达服务器的顺序。

SerializerSettings

如果更改了数据同步服务器上的序列化程序设置,则需要对客户端上的 SerializerSettings 进行相同的更改。 此选项允许你指定自己的序列化程序设置。

TableEndpointResolver

按照约定,表位于 /tables/{tableName} 路径的远程服务上(由服务器代码中的 Route 属性指定)。 但是,表可以存在于任何终结点路径中。 TableEndpointResolver 是一个函数,可将表名转换为与远程服务通信的路径。

例如,以下更改假设,以便所有表都位于 /api下:

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

UserAgent

数据同步客户端根据库的版本生成合适的 User-Agent 标头值。 一些开发人员认为用户代理标头会泄露有关客户端的信息。 可以将 UserAgent 属性设置为任何有效的标头值。

使用远程表

以下部分详细介绍了如何搜索和检索记录以及修改远程表中的数据。 下面介绍了以下主题:

创建远程表引用

若要创建远程表引用,请使用 GetRemoteTable<T>

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

如果要返回只读表,请使用 IReadOnlyRemoteTable<T> 版本:

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

模型类型必须从服务实现 ITableData 协定。 使用 DatasyncClientData 提供必填字段:

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

DatasyncClientData 对象包括:

  • Id (string) - 项的全局唯一 ID。
  • UpdatedAt (System.DataTimeOffset) - 项上次更新的日期/时间。
  • Version (string) - 用于版本控制的非透明字符串。
  • Deleted (boolean) - 如果 true,则会删除该项。

该服务维护这些字段。 不要将这些字段调整为客户端应用程序的一部分。

可以使用 Newtonsoft.JSON 属性对模型进行批注。 可以使用 DataTable 属性指定表的名称:

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

或者,在 GetRemoteTable() 调用中指定表的名称:

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

客户端使用路径 /tables/{tablename} 作为 URI。 表名也是 SQLite 数据库中脱机表的名称。

支持的类型

除了基元类型(int、float、string 等),模型还支持以下类型:

  • System.DateTime - 作为具有 ms 准确性的 ISO-8601 UTC 日期/时间字符串。
  • System.DateTimeOffset - 作为具有 ms 准确性的 ISO-8601 UTC 日期/时间字符串。
  • System.Guid - 格式为 32 位数字,分隔为连字符。

从远程服务器查询数据

远程表可以与 LINQ 类似语句一起使用,包括:

  • 使用 .Where() 子句进行筛选。
  • 使用各种 .OrderBy() 子句进行排序。
  • 选择具有 .Select()的属性。
  • 分页 .Skip().Take()

对查询中的项进行计数

如果需要查询返回的项计数,则可以对表使用 .CountItemsAsync() 或在查询上使用 .LongCountAsync()

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

此方法会导致往返服务器。 还可以在填充列表时获取计数(例如),避免额外的往返:

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

在检索表内容的第一个请求之后,将填充计数。

返回所有数据

数据通过 IAsyncEnumerable返回:

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

使用以下任何终止子句将 IAsyncEnumerable<T> 转换为其他集合:

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

在后台,远程表会为你处理结果的分页。 无论满足查询需要多少个服务器端请求,都会返回所有项。 这些元素也可用于查询结果(例如,remoteTable.Where(m => m.Rating == "R"))。

数据同步框架还提供 ConcurrentObservableCollection<T> - 线程安全的可观测集合。 此类可用于通常使用 ObservableCollection<T> 来管理列表的 UI 应用程序的上下文(例如 Xamarin Forms 或 MAUI 列表)。 可以直接从表或查询中清除和加载 ConcurrentObservableCollection<T>

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

使用 .ToObservableCollection(collection) 将触发整个集合的 CollectionChanged 事件一次,而不是针对单个项,从而缩短重新绘制时间。

ConcurrentObservableCollection<T> 还进行了谓词驱动的修改:

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

如果事先不知道项的索引,则可以在事件处理程序中使用谓词驱动的修改。

筛选数据

可以使用 .Where() 子句筛选数据。 例如:

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

筛选是在 IAsyncEnumerable 之前在服务上完成的,在 IAsyncEnumerable 之后在客户端上完成。 例如:

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

第一个 .Where() 子句(仅返回不完整的项)在服务上执行,而第二个 .Where() 子句(以“The”开头)在客户端上执行。

Where 子句支持转换为 OData 子集的操作。 操作包括:

  • 关系运算符(==!=<<=>>=
  • 算术运算符(+-/*%),
  • 数字精度 (Math.FloorMath.Ceiling),
  • 字符串函数(LengthSubstringReplaceIndexOfEqualsStartsWithEndsWith) (仅限序号和固定区域性),
  • 日期属性(YearMonthDayHourMinuteSecond
  • 访问对象的属性,以及
  • 合并其中任一操作的表达式。

对数据进行排序

通过属性访问器使用 .OrderBy().OrderByDescending().ThenBy().ThenByDescending() 对数据进行排序。

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

排序由服务完成。 不能在任何排序子句中指定表达式。 如果要按表达式排序,请使用客户端排序:

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

选择属性

可以从服务中返回一部分数据:

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

返回数据页

可以使用 .Skip().Take() 返回数据集的子集来实现分页:

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

在实际应用中,可以使用类似于前面的示例的查询和寻呼控件或类似的 UI 在页面之间导航。

到目前为止介绍的所有函数都是累加性的,因此我们可以继续链接它们。 每个链接调用都会影响更多查询。 还有一个示例:

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

按 ID 查找远程数据

GetItemAsync 函数可用于查找具有特定 ID 的数据库中的对象。

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

如果尝试检索的项已被软删除,则必须使用 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);

在远程服务器上插入数据

所有客户端类型都必须包含一个名为 Id的成员,该成员默认为字符串。 执行 CRUD 操作和脱机同步需要此 ID。以下代码演示如何使用 InsertItemAsync 方法将新行插入表中。 该参数包含要作为 .NET 对象插入的数据。

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

如果在插入期间 item 中不包含唯一的自定义 ID 值,服务器将生成 ID。 可以通过在调用返回后检查对象来检索生成的 ID。

更新远程服务器上的数据

以下代码演示如何使用 ReplaceItemAsync 方法通过新信息使用相同的 ID 更新现有记录。

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

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

删除远程服务器上的数据

以下代码演示如何使用 DeleteItemAsync 方法删除现有实例。

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

await todoTable.DeleteItemAsync(item);

冲突解决和乐观并发

两个或多个客户端可以同时将更改写入同一项。 如果没有冲突检测,最后一次写入将覆盖任何以前的更新。 乐观并发控制 假设每个事务都可以提交,因此不使用任何资源锁定。 乐观并发控制在提交数据之前验证没有其他事务修改过数据。 如果数据已修改,则会回滚事务。

Azure 移动应用支持乐观并发控制,方法是使用移动应用后端中为每个表定义的 version 系统属性列跟踪对每个项的更改。 每次更新记录时,移动应用都会将该记录的 version 属性设置为新值。 在每个更新请求期间,请求中包含的记录 version 属性与服务器上的记录的相同属性进行比较。 如果随请求传递的版本与后端不匹配,则客户端库将引发 DatasyncConflictException<T> 异常。 包含异常的类型是包含记录的服务器版本的后端的记录。 然后,应用程序可以使用此信息来决定是否使用后端的正确 version 值再次执行更新请求以提交更改。

使用 DatasyncClientData 基对象时,会自动启用乐观并发。

除了启用乐观并发外,还必须捕获代码中的 DatasyncConflictException<T> 异常。 通过将正确的 version 应用到更新的记录来解决冲突,然后使用已解决的记录重复调用。 以下代码演示如何解决检测到的写入冲突:

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

使用脱机表

脱机表使用本地 SQLite 存储来存储数据,以便在脱机时使用。 针对本地 SQLite 存储而不是远程服务器存储执行所有表操作。 确保将 Microsoft.Datasync.Client.SQLiteStore 添加到每个平台项目和任何共享项目。

在创建表引用之前,必须准备好本地存储:

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

定义存储后,可以创建客户端:

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

最后,必须确保脱机功能已初始化:

await client.InitializeOfflineStoreAsync();

创建客户端后,通常立即完成存储初始化。 FlineConnectionString 是用于指定 SQLite 数据库的位置和用于打开数据库的选项的 URI。 有关详细信息,请参阅 SQLite中的 URI 文件名。

  • 若要使用内存中缓存,请使用 file:inmemory.db?mode=memory&cache=private
  • 若要使用文件,请使用 file:/path/to/file.db

必须指定文件的绝对文件名。 如果使用 Xamarin,可以使用 Xamarin Essentials 文件系统帮助程序 构造路径:例如:

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

如果使用 MAUI,可以使用 MAUI 文件系统帮助程序 构造路径:例如:

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

创建脱机表

可以使用 GetOfflineTable<T> 方法获取表引用:

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

与远程表一样,还可以公开只读脱机表:

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

无需进行身份验证才能使用脱机表。 只需在与后端服务通信时进行身份验证。

同步脱机表

默认情况下,脱机表不会与后端同步。 同步分为两个部分。 可以单独推送更改,而无需下载新项目。 例如:

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

默认情况下,所有表都使用增量同步 - 仅检索新记录。 每个唯一查询都包含一条记录(通过创建 OData 查询的 MD5 哈希生成)。

注意

PullItemsAsync 的第一个参数是 OData 查询,指示要拉取到设备的记录。 最好修改服务以仅返回特定于用户的记录,而不是在客户端上创建复杂的查询。

通常不需要设置选项(由 PullOptions 对象定义)。 选项包括:

  • PushOtherTables - 如果设置为 true,将推送所有表。
  • QueryId - 要使用的特定查询 ID,而不是生成的查询 ID。
  • WriteDeltaTokenInterval - 写入用于跟踪增量同步的增量令牌的频率。

SDK 在拉取记录之前执行隐式 PushAsync()

冲突处理发生在 PullAsync() 方法上。 处理与联机表相同的冲突。 调用 PullAsync() 时,而不是在插入、更新或删除期间生成冲突。 如果发生多个冲突,它们将捆绑到单个 PushFailedException中。 单独处理每个失败。

推送所有表的更改

若要将所有更改推送到远程服务器,请使用:

await client.PushTablesAsync();

若要推送表子集的更改,请为 PushTablesAsync() 方法提供 IEnumerable<string>

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

使用 client.PendingOperations 属性读取等待推送到远程服务的操作数。 未配置脱机存储时,此属性 null

运行复杂的 SQLite 查询

如果需要对脱机数据库执行复杂的 SQL 查询,可以使用 ExecuteQueryAsync() 方法执行此操作。 例如,若要执行 SQL JOIN 语句,请定义一个显示返回值结构的 JObject,然后使用 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.

定义是一组键/值。 这些键必须与 SQL 查询返回的字段名称匹配,并且这些值必须是预期的类型的默认值。 将 0L 用于数字(long)、false 布尔值,并为其他所有项使用 string.Empty

SQLite 具有一组限制性的受支持类型。 日期/时间存储为自纪元以来允许比较的毫秒数。

对用户进行身份验证

Azure 移动应用允许生成用于处理身份验证调用的身份验证提供程序。 在构造服务客户端时指定身份验证提供程序:

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

每当需要身份验证时,将调用身份验证提供程序以获取令牌。 泛型身份验证提供程序可用于基于授权标头的身份验证和应用服务身份验证和基于授权的身份验证。 使用以下模型:

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

身份验证令牌缓存在内存中(从未写入设备),并在必要时进行刷新。

使用Microsoft标识平台

使用Microsoft标识平台,可以轻松地与 Microsoft Entra ID 集成。 有关如何实现 Microsoft Entra 身份验证的完整教程,请参阅快速入门教程。 以下代码演示了检索访问令牌的示例:

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

有关将Microsoft标识平台与 ASP.NET 6 集成的详细信息,请参阅 Microsoft 标识平台 文档。

使用 Xamarin Essentials 或 MAUI WebAuthenticator

对于 Azure 应用服务身份验证,可以使用 Xamarin Essentials WebAuthenticatorMAUI WebAuthenticator 来获取令牌:

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

使用 Azure 应用服务身份验证时,UserIdDisplayName 不直接可用。 请改用延迟请求程序从 /.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);
    }
}

高级主题

清除本地数据库中的实体

在正常操作下,不需要清除实体。 同步过程删除已删除的实体,并维护本地数据库表所需的元数据。 但是,有时清除数据库中的实体会有所帮助。 其中一种情况是需要删除大量实体,并且在本地擦除表中的数据会更高效。

若要清除表中的记录,请使用 table.PurgeItemsAsync()

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

该查询标识要从表中删除的实体。 使用 LINQ 标识要清除的实体:

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

PurgeOptions 类提供用于修改清除操作的设置:

  • DiscardPendingOperations 放弃等待发送到服务器的操作队列中表的任何挂起操作。
  • QueryId 指定用于标识用于操作的增量令牌的查询 ID。
  • TimestampUpdatePolicy 指定如何在清除操作结束时调整增量令牌:
    • TimestampUpdatePolicy.NoUpdate 指示不能更新增量令牌。
    • TimestampUpdatePolicy.UpdateToLastEntity 指示应将增量令牌更新为表中存储的最后一个实体的 updatedAt 字段。
    • TimestampUpdatePolicy.UpdateToNow 指示增量令牌应更新到当前日期/时间。
    • TimestampUpdatePolicy.UpdateToEpoch 指示应重置增量令牌以同步所有数据。

使用调用 table.PullItemsAsync() 同步数据时使用的相同 QueryId 值。 QueryId 指定要在清除完成后更新的增量令牌。

自定义请求标头

若要支持特定应用方案,可能需要自定义与移动应用后端的通信。 例如,可以将自定义标头添加到每个传出请求或更改响应状态代码,然后再返回到用户。 使用自定义 DelegatingHandler,如以下示例所示:

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

启用请求日志记录

还可以使用 DelegatingHandler 添加请求日志记录:

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

监视同步事件

发生同步事件时,该事件将发布到 client.SynchronizationProgress 事件委托。 事件可用于监视同步过程的进度。 定义同步事件处理程序,如下所示:

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

SynchronizationEventArgs 类型定义如下:

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

当属性与同步事件无关时,args 中的属性是 null-1