如何使用适用于 Azure 移动应用的 .NET 客户端库
本指南说明如何使用适用于 Azure 移动应用的 .NET 客户端库执行常见方案。 在任何 .NET 6 或 .NET Standard 2.0 应用程序中使用 .NET 客户端库,包括 MAUI、Xamarin 和 Windows(WPF、UWP 和 WinUI)。
如果你不熟悉 Azure 移动应用,请考虑先完成其中一个快速入门教程:
- AvaloniaUI
- MAUI (Android 和 iOS)
- Uno Platform
- Windows (UWP)
- Windows (WinUI3)
- Windows (WPF)
- Xamarin (Android Native)
- Xamarin (iOS Native)
- Xamarin Forms (Android 和 iOS)
注意
本文介绍 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
此外,还为 Avalonia 和 Uno 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.Floor
、Math.Ceiling
), - 字符串函数(
Length
、、Substring
、IndexOf
Replace
、Equals
、、StartsWith
)EndsWith
(仅限序号和固定区域性), - 日期属性(
Year
、Month
、Day
、Hour
、Minute
、Second
), - 对象的属性访问,以及
- 组合任何这些运算的表达式。
对数据进行排序
使用.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 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);
每当需要身份验证时,将调用身份验证提供程序以获取令牌。 泛型身份验证提供程序可用于基于授权标头的身份验证和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 WebAuthenticator 或 MAUI 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
};
}
UserId
DisplayName
使用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
类提供用于修改清除操作的设置:
DiscardPendingOperations
dis卡等待发送到服务器的操作队列中表的任何挂起操作。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
属性与同步事件无关。
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈