如何使用适用于 .NET 的 Azure 移动应用 v4.2.0 客户端库
注意
此产品已停用。 有关使用 .NET 8 或更高版本的项目的替换,请参阅 Community Toolkit Datasync 库。
本指南介绍如何使用适用于 Azure 移动应用的 .NET 客户端库执行常见方案。 在 Windows(WPF、UWP)或 Xamarin(本机或窗体)应用程序中使用 .NET 客户端库。 如果不熟悉 Azure 移动应用,请考虑先完成 Xamarin.Forms 教程
警告
本文介绍 v4.2.0 库版本的信息,该版本被 v5.0.0 库取代。 有关最新信息,请参阅 最新版本的文章
支持的平台
.NET 客户端库支持 .NET Standard 2.0 和以下平台:
- 从 API 级别 19 到 API 级别 30 的 Xamarin.Android。
- Xamarin.iOS 版本 8.0 到 14.3。
- 通用 Windows 平台版本 16299 及更高版本。
- 任何 .NET Standard 2.0 应用程序。
“服务器流”身份验证对呈现的 UI 使用 WebView,并且可能无法在每个平台上使用。 如果不可用,则必须提供“客户端流”身份验证。 使用身份验证时,此客户端库不适合监视或 IoT 外形规格。
设置和先决条件
我们假设你已创建并发布了 Azure 移动应用后端项目,其中包括至少一个表。 在本主题中使用的代码中,表命名为 TodoItem
,其中包含字符串 Id
,以及 Text
字段和布尔 Complete
列。 此表是在完成 快速入门时创建的同一个表。
C# 中的相应类型化客户端类型是此类:
public class TodoItem
{
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
[JsonProperty(PropertyName = "complete")]
public bool Complete { get; set; }
}
JsonPropertyAttribute 用于定义客户端字段和表字段之间的 PropertyName 映射。
若要了解如何在移动应用后端中创建表,请参阅 .NET Server SDK 主题Node.js 服务器 SDK 主题。
安装托管客户端 SDK 包
右键单击项目,按 管理 NuGet 包,搜索 Microsoft.Azure.Mobile.Client
包,然后按 安装。 对于脱机功能,请安装 Microsoft.Azure.Mobile.Client.SQLiteStore
包。
创建 Azure 移动应用客户端
以下代码创建用于访问移动应用后端的 MobileServiceClient 对象。
var client = new MobileServiceClient("MOBILE_APP_URL");
在前面的代码中,将 MOBILE_APP_URL
替换为应用服务后端的 URL。
MobileServiceClient
对象应为单一实例。
使用表
以下部分详细介绍了如何搜索和检索记录并修改表中的数据。 下面介绍了以下主题:
- 创建表引用
- 查询数据
- 筛选器返回的数据
- 对返回的数据进行排序
- 返回页面 中的数据
- 选择特定列
- 按 ID 查找记录
- 执行非类型化查询
- 插入数据
- 更新数据
- 删除数据
- 冲突解决和乐观并发
- 将数据绑定到 Windows 用户界面
- 更改页面大小
创建表引用
访问或修改后端表中的数据的所有代码都会调用 MobileServiceTable
对象上的函数。 通过调用 GetTable 方法获取对表的引用,如下所示:
IMobileServiceTable<TodoItem> todoTable = client.GetTable<TodoItem>();
返回的对象使用类型化序列化模型。 还支持非类型化序列化模型。 以下示例 创建对非类型化表的引用:
// Get an untyped table reference
IMobileServiceTable untypedTodoTable = client.GetTable("TodoItem");
在非类型化查询中,必须指定基础 OData 查询字符串。
从移动应用查询数据
本部分介绍如何向移动应用后端发出查询,其中包括以下功能:
- 筛选器返回的数据
- 对返回的数据进行排序
- 返回页面 中的数据
- 选择特定列
- 按 ID 查找记录
注意
强制实施服务器驱动的页面大小,以防止返回所有行。 分页使大型数据集的默认请求不会对服务产生负面影响。 若要返回 50 多行,请使用 Skip
和 Take
方法,如 返回页中的数据中所述。
筛选返回的数据
以下代码演示如何通过在查询中包含 Where
子句来筛选数据。 它返回 todoTable
其 Complete
属性等于 false
的所有项。
Where 函数将行筛选谓词应用于针对表的查询。
// This query filters out completed TodoItems and items without a timestamp.
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.ToListAsync();
可以使用消息检查软件(如浏览器开发人员工具或 Fiddler)查看发送到后端的请求的 URI。 如果查看请求 URI,请注意查询字符串已修改:
GET /tables/todoitem?$filter=(complete+eq+false) HTTP/1.1
此 OData 请求由服务器 SDK 转换为 SQL 查询:
SELECT *
FROM TodoItem
WHERE ISNULL(complete, 0) = 0
传递给 Where
方法的函数可以具有任意数量的条件。
// This query filters out completed TodoItems where Text isn't null
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false && todoItem.Text != null)
.ToListAsync();
此示例将由服务器 SDK 转换为 SQL 查询:
SELECT *
FROM TodoItem
WHERE ISNULL(complete, 0) = 0
AND ISNULL(text, 0) = 0
此查询也可以拆分为多个子句:
List<TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.Where(todoItem => todoItem.Text != null)
.ToListAsync();
这两种方法是等效的,可以互换使用。 在一个查询中连接多个谓词的前一个选项更紧凑,建议使用。
Where
子句支持转换为 OData 子集的操作。 操作包括:
- 关系运算符(
==
、!=
、<
、<=
、>
、>=
) - 算术运算符(
+
、-
、/
、*
、%
), - 数字精度 (
Math.Floor
,Math.Ceiling
), - 字符串函数(
Length
、Substring
、Replace
、IndexOf
、StartsWith
、EndsWith
) - 日期属性(
Year
、Month
、Day
、Hour
、Minute
、Second
) - 访问对象的属性,以及
- 合并其中任一操作的表达式。
考虑服务器 SDK 支持的内容时,可以考虑 OData v3 文档。
对返回的数据进行排序
以下代码演示如何通过在查询中包含 OrderBy 或 OrderByDescending 函数对数据进行排序。 它返回 todoTable
按 Text
字段升序排序的项。
// Sort items in ascending order by Text field
MobileServiceTableQuery<TodoItem> query = todoTable
.OrderBy(todoItem => todoItem.Text)
List<TodoItem> items = await query.ToListAsync();
// Sort items in descending order by Text field
MobileServiceTableQuery<TodoItem> query = todoTable
.OrderByDescending(todoItem => todoItem.Text)
List<TodoItem> items = await query.ToListAsync();
在页面中返回数据
默认情况下,后端仅返回前 50 行。 可以通过调用 Take 方法增加返回的行数。 使用 Take
和 Skip 方法请求查询返回的总数据集的特定“页面”。 执行后,以下查询将返回表中的前三项。
// Define a filtered query that returns the top 3 items.
MobileServiceTableQuery<TodoItem> query = todoTable.Take(3);
List<TodoItem> items = await query.ToListAsync();
以下修订后的查询跳过前三个结果,并返回接下来的三个结果。 此查询生成数据的第二个“页面”,其中页面大小为三个项目。
// Define a filtered query that skips the top 3 items and returns the next 3 items.
MobileServiceTableQuery<TodoItem> query = todoTable.Skip(3).Take(3);
List<TodoItem> items = await query.ToListAsync();
IncludeTotalCount 方法请求返回的所有记录 的总计数,忽略指定的任何分页/限制子句:
query = query.IncludeTotalCount();
在实际应用中,可以使用类似于前面的示例的查询和寻呼控件或类似的 UI 在页面之间导航。
注意
若要替代移动应用后端中的 50 行限制,还必须将 EnableQueryAttribute 应用于公共 GET 方法并指定分页行为。 应用于该方法时,以下方法将返回的最大行数设置为 1000:
[EnableQuery(MaxTop=1000)]
选择特定列
可以通过向查询添加 Select 子句来指定要包含在结果中的属性集。 例如,以下代码演示如何仅选择一个字段,以及如何选择和设置多个字段的格式:
// Select one field -- just the Text
MobileServiceTableQuery<TodoItem> query = todoTable
.Select(todoItem => todoItem.Text);
List<string> items = await query.ToListAsync();
// Select multiple fields -- both Complete and Text info
MobileServiceTableQuery<TodoItem> query = todoTable
.Select(todoItem => string.Format("{0} -- {1}",
todoItem.Text.PadRight(30), todoItem.Complete ?
"Now complete!" : "Incomplete!"));
List<string> items = await query.ToListAsync();
到目前为止介绍的所有函数都是累加性的,因此我们可以继续链接它们。 每个链接调用都会影响更多查询。 还有一个示例:
MobileServiceTableQuery<TodoItem> query = todoTable
.Where(todoItem => todoItem.Complete == false)
.Select(todoItem => todoItem.Text)
.Skip(3).
.Take(3);
List<string> items = await query.ToListAsync();
按 ID 查找数据
LookupAsync 函数可用于查找具有特定 ID 的数据库中的对象。
// This query filters out the item with the ID of 37BBF396-11F0-4B39-85C8-B319C729AF6D
TodoItem item = await todoTable.LookupAsync("37BBF396-11F0-4B39-85C8-B319C729AF6D");
执行非类型化查询
使用非类型化表对象执行查询时,必须通过调用 ReadAsync显式指定 OData 查询字符串,如以下示例所示:
// Lookup untyped data using OData
JToken untypedItems = await untypedTodoTable.ReadAsync("$filter=complete eq 0&$orderby=text");
可以恢复可以像属性包一样使用的 JSON 值。 有关 JToken
和 Newtonsoft Json 的详细信息,请参阅 Newtonsoft JSON 站点。
插入数据
所有客户端类型都必须包含一个名为 Id的成员,该成员默认为字符串。 执行 CRUD 操作和脱机同步需要此 ID。以下代码演示如何使用 InsertAsync 方法将新行插入表中。 该参数包含要作为 .NET 对象插入的数据。
await todoTable.InsertAsync(todoItem);
如果在插入期间 todoItem
中不包含唯一的自定义 ID 值,则服务器将生成 GUID。 可以通过在调用返回后检查对象来检索生成的 ID。
若要插入非类型化数据,可以利用 Json.NET:
JObject jo = new JObject();
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.InsertAsync(jo);
下面是使用电子邮件地址作为唯一字符串 ID 的示例:
JObject jo = new JObject();
jo.Add("id", "myemail@emaildomain.com");
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.InsertAsync(jo);
使用 ID 值
移动应用支持表 ID 列的唯一自定义字符串值。 字符串值允许应用程序使用自定义值,例如 ID 的电子邮件地址或用户名。 字符串 ID 提供以下优势:
- 生成 ID,而无需往返数据库。
- 记录更易于从不同的表或数据库合并。
- ID 值可以更好地与应用程序的逻辑集成。
如果未在插入的记录上设置字符串 ID 值,移动应用后端将为 ID 生成唯一值。 可以使用 Guid.NewGuid 方法在客户端或后端生成自己的 ID 值。
JObject jo = new JObject();
jo.Add("id", Guid.NewGuid().ToString("N"));
更新数据
以下代码演示了如何使用 UpdateAsync 方法使用新信息的相同 ID 更新现有记录。 该参数包含要作为 .NET 对象更新的数据。
await todoTable.UpdateAsync(todoItem);
若要更新非类型化数据,可以利用 Newtonsoft JSON,如下所示:
JObject jo = new JObject();
jo.Add("id", "37BBF396-11F0-4B39-85C8-B319C729AF6D");
jo.Add("Text", "Hello World");
jo.Add("Complete", false);
var inserted = await table.UpdateAsync(jo);
在进行更新时,必须指定 id
字段。 后端使用 id
字段标识要更新的行。 可以从 InsertAsync
调用的结果获取 id
字段。 如果尝试在不提供 id
值的情况下更新项,则会引发 ArgumentException
。
删除数据
以下代码演示如何使用 DeleteAsync 方法删除现有实例。 实例由 todoItem
上设置的 id
字段标识。
await todoTable.DeleteAsync(todoItem);
若要删除非类型化数据,可以利用 Json.NET,如下所示:
JObject jo = new JObject();
jo.Add("id", "37BBF396-11F0-4B39-85C8-B319C729AF6D");
await table.DeleteAsync(jo);
发出删除请求时,必须指定 ID。 其他属性不会传递给服务,也不会在服务中忽略。
DeleteAsync
调用的结果通常 null
。 可以通过 InsertAsync
调用的结果获取传入的 ID。 尝试删除项而不指定 id
字段时,将引发 MobileServiceInvalidOperationException
。
冲突解决和乐观并发
两个或多个客户端可能会同时将更改写入同一项。 如果没有冲突检测,最后一次写入将覆盖任何以前的更新。 乐观并发控制 假设每个事务都可以提交,因此不使用任何资源锁定。 在提交事务之前,乐观并发控制会验证没有其他事务修改数据。 如果数据已修改,则回滚提交事务。
移动应用支持乐观并发控制,方法是使用移动应用后端中为每个表定义的 version
系统属性列跟踪对每个项的更改。 每次更新记录时,移动应用都会将该记录的 version
属性设置为新值。 在每个更新请求期间,请求中包含的记录 version
属性与服务器上的记录的相同属性进行比较。 如果使用请求传递的版本与后端不匹配,则客户端库将引发 MobileServicePreconditionFailedException<T>
异常。 包含异常的类型是包含记录的服务器版本的后端的记录。 然后,应用程序可以使用此信息来决定是否使用后端的正确 version
值再次执行更新请求以提交更改。
为 version
系统属性定义表类上的列,以启用乐观并发。 例如:
public class TodoItem
{
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
[JsonProperty(PropertyName = "complete")]
public bool Complete { get; set; }
// *** Enable Optimistic Concurrency *** //
[JsonProperty(PropertyName = "version")]
public string Version { set; get; }
}
使用非类型化表的应用程序通过在表的 SystemProperties
上设置 Version
标志来启用乐观并发,如下所示。
//Enable optimistic concurrency by retrieving version
todoTable.SystemProperties |= MobileServiceSystemProperties.Version;
除了启用乐观并发外,还必须在 调用 updateAsync时捕获代码中的 MobileServicePreconditionFailedException<T>
异常。 通过将正确的 version
应用到更新的记录,并使用已解决的记录调用 updateAsync 来解决冲突。 以下代码演示如何解决检测到的写入冲突:
private async void UpdateToDoItem(TodoItem item)
{
MobileServicePreconditionFailedException<TodoItem> exception = null;
try
{
//update at the remote table
await todoTable.UpdateAsync(item);
}
catch (MobileServicePreconditionFailedException<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();
}
有关详细信息,请参阅 Azure 移动应用 主题中的
将数据绑定到 Windows 用户界面
本部分介绍如何在 Windows 应用中使用 UI 元素显示返回的数据对象。 以下示例代码将绑定到列表的源,并查询不完整的项。 MobileServiceCollection 创建移动应用感知绑定集合。
// This query filters out completed TodoItems.
MobileServiceCollection<TodoItem, TodoItem> items = await todoTable
.Where(todoItem => todoItem.Complete == false)
.ToCollectionAsync();
// itemsControl is an IEnumerable that could be bound to a UI list control
IEnumerable itemsControl = items;
// Bind this to a ListBox
ListBox lb = new ListBox();
lb.ItemsSource = items;
托管运行时中的某些控件支持名为 ISupportIncrementalLoading的接口。 此接口允许控件在用户滚动时请求额外数据。 通过 MobileServiceIncrementalLoadingCollection为通用 Windows 应用提供此接口的内置支持,该接口会自动处理来自控件的调用。 在 Windows 应用中使用 MobileServiceIncrementalLoadingCollection
,如下所示:
MobileServiceIncrementalLoadingCollection<TodoItem,TodoItem> items;
items = todoTable.Where(todoItem => todoItem.Complete == false).ToIncrementalLoadingCollection();
ListBox lb = new ListBox();
lb.ItemsSource = items;
若要在 Windows Phone 8 和“Silverlight”应用上使用新集合,请使用 IMobileServiceTableQuery<T>
和 IMobileServiceTable<T>
上的 ToCollection
扩展方法。 若要加载数据,请调用 LoadMoreItemsAsync()
。
MobileServiceCollection<TodoItem, TodoItem> items = todoTable.Where(todoItem => todoItem.Complete==false).ToCollection();
await items.LoadMoreItemsAsync();
使用通过调用 ToCollectionAsync
或 ToCollection
创建的集合时,将获得可以绑定到 UI 控件的集合。 此集合可识别分页。 由于集合正在从网络加载数据,因此加载有时会失败。 若要处理此类故障,请重写 MobileServiceIncrementalLoadingCollection
上的 OnException
方法以处理调用 LoadMoreItemsAsync
导致的异常。
请考虑表是否具有许多字段,但只想在控件中显示其中一些字段。 可以使用上一部分中的指南“选择特定列”来选择要在 UI 中显示的特定列。
更改页面大小
默认情况下,Azure 移动应用为每个请求返回最多 50 个项目。 可以通过增加客户端和服务器上的最大页面大小来更改分页大小。 若要增加请求的页面大小,请在使用 PullAsync()
时指定 PullOptions
:
PullOptions pullOptions = new PullOptions
{
MaxPageSize = 100
};
假设 PageSize
在服务器中等于或大于 100,则请求最多返回 100 个项目。
使用脱机表
脱机表使用本地 SQLite 存储来存储数据,以便在脱机时使用。 针对本地 SQLite 存储而不是远程服务器存储执行所有表操作。 若要创建脱机表,请先准备项目。
- 在 Visual Studio 中,右键单击解决方案 >管理解决方案的 NuGet 包...,然后搜索并安装解决方案中所有项目的 Microsoft.Azure.Mobile.Client.SQLiteStore NuGet 包。
- 对于 Windows 设备,按
引用 添加引用... ,展开windows 文件夹扩展 ,然后为 Windows SDK 启用相应的SQLite 以及适用于 Windows SDK 的 Visual C++ 2013 Runtime。 SQLite SDK 名称因每个 Windows 平台而略有不同。
在创建表引用之前,必须准备好本地存储:
var store = new MobileServiceSQLiteStore(Constants.OfflineDbPath);
store.DefineTable<TodoItem>();
//Initializes the SyncContext using the default IMobileServiceSyncHandler.
await this.client.SyncContext.InitializeAsync(store);
创建客户端后,通常立即完成存储初始化。 OfflineDbPath 应该是适合在支持的所有平台上使用的文件名。 如果路径是完全限定的路径(即它以斜杠开头),则使用该路径。 如果路径未完全限定,该文件将放置在特定于平台的位置。
- 对于 iOS 和 Android 设备,默认路径为“个人文件”文件夹。
- 对于 Windows 设备,默认路径是特定于应用程序的“AppData”文件夹。
可以使用 GetSyncTable<>
方法获取表引用:
var table = client.GetSyncTable<TodoItem>();
无需进行身份验证才能使用脱机表。 只需在与后端服务通信时进行身份验证。
同步脱机表
默认情况下,脱机表不会与后端同步。 同步分为两个部分。 可以单独推送更改,而无需下载新项目。 下面是典型的同步方法:
public async Task SyncAsync()
{
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
await this.client.SyncContext.PushAsync();
await this.todoTable.PullAsync(
//The first parameter is a query name that is used internally by the client SDK to implement incremental sync.
//Use a different query name for each unique query in your program
"allTodoItems",
this.todoTable.CreateQuery());
}
catch (MobileServicePushFailedException exc)
{
if (exc.PushResult != null)
{
syncErrors = exc.PushResult.Errors;
}
}
// Simple error/conflict handling. A real application would handle the various errors like network conditions,
// server conflicts and others via the IMobileServiceSyncHandler.
if (syncErrors != null)
{
foreach (var error in syncErrors)
{
if (error.OperationKind == MobileServiceTableOperationKind.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"]);
}
}
}
如果 PullAsync
的第一个参数为 null,则不会使用增量同步。 每个同步操作将检索所有记录。
SDK 在拉取记录之前执行隐式 PushAsync()
。
冲突处理发生在 PullAsync()
方法上。 可以像处理联机表一样处理冲突。 调用 PullAsync()
时,而不是在插入、更新或删除期间生成冲突。 如果发生多个冲突,它们将捆绑到单个 MobileServicePushFailedException 中。 单独处理每个失败。
使用自定义 API
使用自定义 API 可以定义公开不映射到插入、更新、删除或读取操作的服务器功能的自定义终结点。 通过使用自定义 API,可以更好地控制消息传送,包括读取和设置 HTTP 消息标头以及定义 JSON 以外的消息正文格式。
通过调用客户端上的某个 InvokeApiAsync 方法来调用自定义 API。 例如,以下代码行将 POST 请求发送到后端上的 completeAll API:
var result = await client.InvokeApiAsync<MarkAllResult>("completeAll", System.Net.Http.HttpMethod.Post, null);
此窗体是一个类型化的方法调用,需要定义 MarkAllResult 返回类型。 支持类型化和非类型化方法。
除非 API 以“/”开头,否则 InvokeApiAsync() 方法将“/api/”追加到要调用的 API。 例如:
-
InvokeApiAsync("completeAll",...)
后端调用 /api/completeAll -
InvokeApiAsync("/.auth/me",...)
在后端调用 /.auth/me
可以使用 InvokeApiAsync 调用任何 WebAPI,包括未使用 Azure 移动应用定义的 WebAPI。 使用 InvokeApiAsync()时,会随请求一起发送相应的标头,包括身份验证标头。
对用户进行身份验证
移动应用支持使用各种外部标识提供者对应用用户进行身份验证和授权:Facebook、Google、Microsoft 帐户、Twitter 和Microsoft Entra ID。 可以设置对表的权限,以将特定操作的访问限制为仅经过身份验证的用户。 还可以使用经过身份验证的用户的标识在服务器脚本中实现授权规则。
支持两个身份验证流:客户端管理的 和 服务器管理的 流。 服务器管理的流提供最简单的身份验证体验,因为它依赖于提供程序的 Web 身份验证接口。 客户端托管流允许更深入地与特定于设备的功能集成,因为它依赖于特定于提供程序的设备特定 SDK。
注意
建议在生产应用中使用客户端管理的流。
若要设置身份验证,必须将应用注册到一个或多个标识提供者。 标识提供者为应用生成客户端 ID 和客户端密码。 然后,在后端设置这些值以启用 Azure 应用服务身份验证/授权。
本节介绍了以下主题:
客户端管理的身份验证
应用可以独立联系标识提供者,然后在使用后端登录期间提供返回的令牌。 通过此客户端流,可以为用户提供单一登录体验,或从标识提供者检索额外的用户数据。 客户端流身份验证首选使用服务器流,因为标识提供者 SDK 提供了更本机的 UX 感觉,并允许进行更多自定义。
为以下客户端流身份验证模式提供了示例:
使用 Active Directory 身份验证库对用户进行身份验证
可以使用 Active Directory 身份验证库 (ADAL) 通过 Microsoft Entra 身份验证从客户端启动用户身份验证。
警告
对 Active Directory 身份验证库(ADAL)的支持将于 2022 年 12 月结束。 对现有 OS 版本使用 ADAL 的应用将继续工作,但技术支持和安全更新将结束。 有关详细信息,请参阅 将应用迁移到 MSAL。
按照 如何为 Active Directory 登录配置应用服务 教程,为 Microsoft Entra 登录配置移动应用后端。 请确保完成注册本机客户端应用程序的可选步骤。
在 Visual Studio 中,打开项目并添加对
Microsoft.IdentityModel.Clients.ActiveDirectory
NuGet 包的引用。 搜索时,包括预发行版本。根据所使用的平台,将以下代码添加到应用程序。 在每一个中,进行以下替换:
将 INSERT-AUTHORITY-HERE 替换为在其中预配应用程序的租户的名称。 格式应
https://login.microsoftonline.com/contoso.onmicrosoft.com
。 可以从 [Azure 门户] 中Microsoft Entra ID 中的“域”选项卡复制此值。将 INSERT-RESOURCE-ID-HERE 替换为移动应用后端的客户端 ID。 可以从门户中 Microsoft Entra 设置 下的“高级”选项卡获取客户端 ID。
将 INSERT-CLIENT-ID-HERE 替换为从本机客户端应用程序复制的客户端 ID。
使用 HTTPS 方案将 INSERT-REDIRECT-URI-HERE 替换为站点的
/.auth/login/done
终结点。 此值应类似于https://contoso.azurewebsites.net/.auth/login/done
。每个平台所需的代码如下:
Windows:
private MobileServiceUser user; private async Task AuthenticateAsync() { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; while (user == null) { string message; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto, false) ); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await App.MobileService.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); message = string.Format("You are now logged in - {0}", user.UserId); } catch (InvalidOperationException) { message = "You must log in. Login Required"; } var dialog = new MessageDialog(message); dialog.Commands.Add(new UICommand("OK")); await dialog.ShowAsync(); } }
Xamarin.iOS
private MobileServiceUser user; private async Task AuthenticateAsync(UIViewController view) { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(view)); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await client.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); } catch (Exception ex) { Console.Error.WriteLine(@"ERROR - AUTHENTICATION FAILED {0}", ex.Message); } }
Xamarin.Android
private MobileServiceUser user; private async Task AuthenticateAsync() { string authority = "INSERT-AUTHORITY-HERE"; string resourceId = "INSERT-RESOURCE-ID-HERE"; string clientId = "INSERT-CLIENT-ID-HERE"; string redirectUri = "INSERT-REDIRECT-URI-HERE"; try { AuthenticationContext ac = new AuthenticationContext(authority); AuthenticationResult ar = await ac.AcquireTokenAsync(resourceId, clientId, new Uri(redirectUri), new PlatformParameters(this)); JObject payload = new JObject(); payload["access_token"] = ar.AccessToken; user = await client.LoginAsync( MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload); } catch (Exception ex) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.SetMessage(ex.Message); builder.SetTitle("You must log in. Login Required"); builder.Create().Show(); } } protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { base.OnActivityResult(requestCode, resultCode, data); AuthenticationAgentContinuationHelper.SetAuthenticationAgentContinuationEventArgs(requestCode, resultCode, data); }
使用 Facebook 或 Google 令牌进行单一登录
可以使用适用于 Facebook 或 Google 的此代码片段所示的客户端流。
var token = new JObject();
// Replace access_token_value with actual value of your access token obtained
// using the Facebook or Google SDK.
token.Add("access_token", "access_token_value");
private MobileServiceUser user;
private async Task AuthenticateAsync()
{
while (user == null)
{
string message;
try
{
// Change MobileServiceAuthenticationProvider.Facebook
// to MobileServiceAuthenticationProvider.Google if using Google auth.
user = await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook, token);
message = string.Format("You are now logged in - {0}", user.UserId);
}
catch (InvalidOperationException)
{
message = "You must log in. Login Required";
}
var dialog = new MessageDialog(message);
dialog.Commands.Add(new UICommand("OK"));
await dialog.ShowAsync();
}
}
服务器管理的身份验证
注册标识提供者后,使用提供程序的 MobileServiceAuthenticationProvider 值调用 MobileServiceClient 上的 LoginAsync 方法。 例如,以下代码使用 Facebook 启动服务器流登录。
private MobileServiceUser user;
private async System.Threading.Tasks.Task Authenticate()
{
while (user == null)
{
string message;
try
{
user = await client
.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
message =
string.Format("You are now logged in - {0}", user.UserId);
}
catch (InvalidOperationException)
{
message = "You must log in. Login Required";
}
var dialog = new MessageDialog(message);
dialog.Commands.Add(new UICommand("OK"));
await dialog.ShowAsync();
}
}
如果使用 Facebook 以外的标识提供者,请将 MobileServiceAuthenticationProvider 的值更改为提供程序的值。
在服务器流中,Azure 应用服务通过显示所选提供程序的登录页来管理 OAuth 身份验证流。 标识提供者返回后,Azure 应用服务将生成应用服务身份验证令牌。
注意
在幕后,Azure 移动应用使用 Xamarin.Essentials WebAuthenticator 来执行这项工作。 必须通过调用回 Xamarin.Essentials 来处理来自服务的响应。 有关详细信息,请参阅 WebAuthenticator。
缓存身份验证令牌
在某些情况下,可以通过存储来自提供程序的身份验证令牌,在首次成功身份验证后避免对登录方法的调用。 Microsoft存储和 UWP 应用可以使用 PasswordVault 在成功登录后缓存当前身份验证令牌,如下所示:
await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
PasswordVault vault = new PasswordVault();
vault.Add(new PasswordCredential("Facebook", client.currentUser.UserId,
client.currentUser.MobileServiceAuthenticationToken));
UserId 值存储为凭据的 UserName,令牌存储为密码。 在后续的启动中,可以检查 PasswordVault 是否有缓存凭据。 以下示例在找到缓存凭据时使用缓存凭据,否则会尝试使用后端再次进行身份验证:
// Try to retrieve stored credentials.
var creds = vault.FindAllByResource("Facebook").FirstOrDefault();
if (creds != null)
{
// Create the current user from the stored credentials.
client.currentUser = new MobileServiceUser(creds.UserName);
client.currentUser.MobileServiceAuthenticationToken =
vault.Retrieve("Facebook", creds.UserName).Password;
}
else
{
// Regular login flow and cache the token as shown above.
}
注销用户时,还必须删除存储的凭据,如下所示:
client.Logout();
vault.Remove(vault.Retrieve("Facebook", client.currentUser.UserId));
使用客户端管理的身份验证时,还可以缓存从提供商(如 Facebook 或 Twitter)获取的访问令牌。 可以提供此令牌以从后端请求新的身份验证令牌,如下所示:
var token = new JObject();
// Replace <your_access_token_value> with actual value of your access token
token.Add("access_token", "<your_access_token_value>");
// Authenticate using the access token.
await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook, token);
其他主题
处理错误
当后端发生错误时,客户端 SDK 将引发 MobileServiceInvalidOperationException
。 以下示例演示如何处理后端返回的异常:
private async void InsertTodoItem(TodoItem todoItem)
{
// This code inserts a new TodoItem into the database. When the operation completes
// and App Service has assigned an ID, the item is added to the CollectionView
try
{
await todoTable.InsertAsync(todoItem);
items.Add(todoItem);
}
catch (MobileServiceInvalidOperationException e)
{
// Handle error
}
}
自定义请求标头
若要支持特定应用方案,可能需要自定义与移动应用后端的通信。 例如,你可能希望将自定义标头添加到每个传出请求,甚至更改响应状态代码。 可以使用自定义 DelegatingHandler,如以下示例所示:
public async Task CallClientWithHandler()
{
MobileServiceClient client = new MobileServiceClient("AppUrl", new MyHandler());
IMobileServiceTable<TodoItem> todoTable = client.GetTable<TodoItem>();
var newItem = new TodoItem { Text = "Hello world", Complete = false };
await todoTable.InsertAsync(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;
}
}