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

本指南说明如何使用适用于 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 Core 后端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

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

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

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 。 此选项允许你指定自己的序列化程序设置。

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),
  • 字符串函数(Length、、SubstringIndexOfReplaceEquals、、StartsWithEndsWith(仅限序号和固定区域性),
  • 日期属性(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();

返回数据页

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

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

存储初始化通常会在创建客户端之后立即完成。 Offline连接ionString 是一个 URI,用于指定 SQLite 数据库的位置以及用于打开数据库的选项。 有关详细信息,请参阅 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();

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

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

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

运行复杂的 SQLite 查询

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

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

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 App 服务身份验证,可以使用 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
    };
}

UserIdDisplayName使用Azure App 服务身份验证时不能直接使用。 请改用延迟请求程序从 /.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 类提供用于修改清除操作的设置:

  • DiscardPendingOperationsdis卡等待发送到服务器的操作队列中表的任何挂起操作。
  • 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 属性与同步事件无关。