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

本指南说明如何使用适用于 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 使用 Web 视图,“服务器流”身份验证可能并不会在每个平台上均可用。 如果不可用,则你必须提供“客户端流”身份验证。 使用身份验证时,此客户端库不适用于监视或 IoT 外形规格。

安装与先决条件

假设已创建并发布 Azure 移动应用后端项目(至少包含一个表)。 在本主题使用的代码中,表的名称为 TodoItem,它具有字符串 IdText 字段和布尔值 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 服务器 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 对象应为单一实例。

使用表

以下部分详细说明如何搜索和检索记录,以及修改表中的数据。 论述了以下主题:

创建表引用

访问或修改后端表中数据的所有代码均将调用 MobileServiceTable 对象的函数。 可通过调用 GetTable 方法获取表引用,如下所示:

IMobileServiceTable<TodoItem> todoTable = client.GetTable<TodoItem>();

返回的对象使用类型化的序列化模型。 也支持非类型化的序列化模型。 下例创建对非类型化表的引用

// Get an untyped table reference
IMobileServiceTable untypedTodoTable = client.GetTable("TodoItem");

在非类型化查询中,必须指定基础 OData 查询字符串。

从移动应用查询数据

本部分介绍如何向包含以下功能的移动应用后端发出查询:

注意

将强制使用服务器驱动的页大小来防止返回所有行。 分页可以防止对大型数据集发出的默认请求对服务造成负面影响。 若要返回 50 个以上的行,请根据按页返回数据所述使用 SkipTake 方法。

筛选返回的数据

以下代码演示了如何通过在查询中包含 Where 子句来筛选数据。 该代码将返回 Complete 属性等于 falsetodoTable 中的所有项。 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

服务器 SDK 将此 OData 请求转换成 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.FloorMath.Ceiling),
  • 字符串函数(Length, SubstringReplaceIndexOfStartsWithEndsWith),
  • 日期属性(YearMonthDayHourMinuteSecond),
  • 对象的属性访问,以及
  • 组合任何这些运算的表达式。

在考虑服务器 SDK 支持的运算时,可以参考 OData v3 文档

对返回的数据进行排序

以下代码演示了如何通过在查询中包含 OrderByOrderByDescending 函数来为数据排序。 该代码将返回 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 方法来增加返回的行数。 将 TakeSkip 方法一起使用可以请求查询返回的总数据集的特定“页”。 执行以下查询后,将返回表中的前三个项。

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

使用通过调用 ToCollectionAsyncToCollection 创建的集合时,可以获取可绑定到 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 以及 Visual C++ 2013 Runtime for Windows SDK。 每个 Windows 平台的 SQLite SDK 名称略有不同。

必须先准备本地存储,之后才能创建表引用:

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 设备,默认路径为“Personal Files”文件夹。
  • 对于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。 例如,以下代码行向后端上的 completeAll API 发送 POST 请求:

var result = await client.InvokeApiAsync<MarkAllResult>("completeAll", System.Net.Http.HttpMethod.Post, null);

这种类型化方法调用要求定义 MarkAllResult 返回类型。 支持类型化和非类型化的方法。

InvokeApiAsync() 方法在想要调用的 API 前附加“/api/”,除非 API 以“/”开头。 例如:

  • InvokeApiAsync("completeAll",...) 在后端上调用 /api/completeAll
  • InvokeApiAsync("/.auth/me",...) 在后端上调用 /.auth/me

可使用 InvokeApiAsync 调用任意 WebAPI,包括未使用 Azure 移动应用定义的 WebAPI。 使用 InvokeApiAsync() 时,将随请求一起发送相应的标头(包括身份验证标头)。

对用户进行身份验证

移动应用支持使用各种外部标识提供者对应用用户进行身份验证和授权:Facebook、Google、Microsoft 帐户、Twitter 和 Microsoft Entra ID。 可以在表中设置权限,以便将特定操作的访问权限限制给已经过身份验证的用户。 还可以在服务器脚本中使用已经过身份验证的用户的标识来实施授权规则。

支持两种身份验证流:client-managedserver-managed 流。 服务器托管的流依赖于提供者的 Web 身份验证界面,因此可提供最简便的身份验证体验。 客户端托管的流依赖于提供者和设备特定的 SDK,因此允许与设备特定的功能进行更深入的集成。

注意

建议在生产应用中使用客户端托管流。

若要设置身份验证,必须向一个或多个标识提供者注册应用。 标识提供者为应用生成客户端 ID 和客户端机密。 随后在后端设置这些值,以启用 Azure 应用服务身份验证/授权。

本部分介绍以下主题:

客户端托管的身份验证

应用可以独立联系标识提供者,并在用后端登录期间提供返回的令牌。 使用此客户端流可为用户提供单一登录体验,或者从标识提供者中检索其他用户数据。 客户端流身份验证首选使用服务器流作为标识提供者 SDK,因为它提供更直观的 UX 风格,并可以进行更多自定义。

提供了以下客户端流身份验证模式的示例:

使用 Active Directory 身份验证库对用户进行身份验证

可以使用 Active Directory 身份验证库 (ADAL) 通过 Microsoft Entra 身份验证从客户端启动用户身份验证。

警告

对 Active Directory 身份验证库 (ADAL) 的支持将于 2022 年 12 月结束。 在现有 OS 版本上使用 ADAL 的应用将继续正常运行,但技术支持或安全更新将结束。 有关详细信息,请参阅将应用迁移到 MSAL

  1. 按照如何为 Active Directory 登录教程配置 App 服务,为 Microsoft Entra 登录配置移动应用后端。 请务必完成注册本机客户端应用程序的可选步骤。

  2. 在 Visual Studio 中打开项目,并添加对 Microsoft.IdentityModel.Clients.ActiveDirectory NuGet 包的引用。 搜索时,请包含预发行版。

  3. 根据使用的平台,将以下代码添加到应用程序。 在每条代码中进行以下替换:

    • 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 App Service 将生成应用服务身份验证令牌。 LoginAsync 方法返回 MobileServiceUser,后者提供已经过身份验证的用户的 UserId,以及 JSON Web 令牌 (JWT) 形式的 MobileServiceAuthenticationToken。 可以缓存此令牌,并在它过期之前重复使用。 有关详细信息,请参阅缓存身份验证令牌

注意

实际上,Azure 移动应用使用 Xamarin.Essentials WebAuthenticator 来完成工作。 必须通过调用回 Xamarin.Essentials 来处理来自服务的响应。 有关详细信息,请参阅 WebAuthenticator

缓存身份验证令牌

在某些情况下,存储来自提供者的身份验证令牌可避免在首次成功身份验证后调用登录方法。 Microsoft Store 和 UWP 应用可以使用 PasswordVault 在成功登录后缓存当前身份验证令牌,如下所示:

await client.LoginAsync(MobileServiceAuthenticationProvider.Facebook);

PasswordVault vault = new PasswordVault();
vault.Add(new PasswordCredential("Facebook", client.currentUser.UserId,
    client.currentUser.MobileServiceAuthenticationToken));

UserId 值存储为凭据的 UserName,令牌存储为 Password。 在后续启动时,可以检查 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;
    }
}