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

本指南说明如何使用适用于 Azure 移动应用的 .NET 客户端库执行常见方案。 在 iOS、Android、.NET MAUI、Windows (WPF、UWP、Windows 应用 SDK中使用 iOS、Android、.NET MAUI、UWP Windows 应用 SDK和 WinUI 3) 或 Xamarin/Xamarin.Forms 应用程序。

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

注意

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

受支持的平台

.NET 客户端库支持 .NET Standard 2.0、.NET 6 和以下平台:

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

TodoApp 示例包含每个测试平台的示例。

安装与先决条件

从 NuGet 添加以下库:

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

创建服务客户端

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

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

在前面的代码中,请替换为 MOBILE_APP_URLASP.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

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

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

IdGenerator

将实体添加到脱机表时,它必须具有 ID。 如果未提供 ID,则会生成 ID。 使用 IdGenerator 此选项可以定制生成的 ID。 默认情况下,将生成全局唯一 ID。

InstallationId

自定义标头 X-ZUMO-INSTALLATION-ID 随每个请求一起发送,以标识特定设备上的应用程序组合。 可以在日志中记录此标头,并允许确定应用的不同安装数。 默认情况下,首次启动应用时,会为你生成安装 ID,并保存在永久性存储中。 但是,可以修改属性 InstallationId 以设置自己的安装 ID。 如果设置为空字符串,则不会将标头发送到服务器。

例如,以下设置将生成包含表名称和 GUID 的字符串:

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

脱机存储

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

ParallelOperations

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

SerializerSettings

如果更改了 datasync 服务器上的序列化程序设置,则还需要对 SerializerSettings 客户端上的相同更改。 使用此选项可以指定自己的序列化程序设置。

TableEndpointResolver

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

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

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

UserAgent

Datasync 客户端根据库的版本和平台信息生成合适的User-Agent标头值。 一些开发人员觉得这会泄露信息。 可以将属性 UserAgent 设置为任何有效的标头值。

使用远程表

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

创建远程表引用

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

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

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

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

DatasyncClientData 对象包括:

  • Id (字符串) - 项的全局唯一 ID。
  • UpdatedAt (System.DataTimeOffset) - 项上次更新的日期/时间。
  • Version (字符串) - 用于版本控制的非透明字符串。
  • Deleted (布尔) - 如果 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()

返回所有数据

通过 [IAsyncEnumerable] 返回数据:

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

此外,还可以使用 System.Linq.Async 包中 IAsyncEnumerable 的任何终止子句:

var items = await remoteTable.ToAsyncEnumerable().ToListAsync();

在后台,远程表将为你处理结果的分页。 无论需要多少服务器端请求才能完成查询,都将返回所有项。

筛选数据

可以使用子 .Where() 句来筛选数据。 多个 .Where() 子句与“AND”组合在一起。 例如:

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、、、SubstringReplaceStartsWithIndexOfEqualsEndsWith) (序号和固定区域性) ,
  • 日期属性(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 使用类似于上面的查询,以在页之间导航。

到目前为止所述的所有函数都是累加式的,因此我们可以保留它们的链接。 每个链接的调用都会影响多个查询。 再提供一个示例:

MobileServiceTableQuery<TodoItem> 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");

在远程服务器上插入数据

所有客户端类型必须包含名为 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 值,则服务器将生成 GUID。 通过在调用返回后检查该对象,可以检索生成的 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();

存储初始化通常会在创建客户端之后立即完成。 OfflineConnectionString 是一个 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");

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

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

创建脱机表

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

var 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。

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

使用 PullAsync() 方法时需进行冲突处理。 以与联机表相同的方式处理冲突。 冲突在调用 PullAsync() 时(而不是在插入、更新或生成期间)产生。 如果发生多个冲突,它们将捆绑到单个 PushFailedException冲突中。 单独处理每个故障。

推送所有表的更改

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

await client.PushTablesAsync();

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

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

运行复杂的 SQLite 查询

如果需要对脱机数据库执行复杂的 SQL 查询,可以使用该方法 ExecuteQueryAsync() 执行此操作。 例如,若要执行语句 SQL JOIN ,请定义返回值窗体,然后使用 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 (长) 的数字、 false 布尔值,以及用于其他所有内容的字符串。 SQLite 具有一组限制性的类型。 日期/时间存储为数值 (作为 ms,因为纪元) 进行比较。

对用户进行身份验证

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 标识平台允许你轻松与 Azure Active Directory 集成。 有关如何实现 Azure Active Directory 身份验证的完整教程,请参阅快速入门教程。 以下代码演示检索访问令牌的示例:

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 Identity Platform 与 ASP.NET 6 集成的详细信息,请参阅 Microsoft 标识平台 文档。

使用 Xamarin.Essentials 或 .NET MAUI WebAuthenticator

对于Azure 应用服务身份验证,可以使用 Xamarin.Essentials WebAuthenticator.NET 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使用Azure 应用服务身份验证时,并且DisplayName不能直接使用。 请改用延迟请求程序从 /.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);
    }
}

高级主题

自定义请求标头

若要支持特定的应用程序方案,可能需要自定义与移动应用后端之间的通信。 例如,可能需要将一个自定义标头添加到每个传出请求,甚至要更改响应状态代码。 可以使用自定义 DelegatingHandler 来实现此目的,如以下示例中所示:

public async Task CallClientWithHandler()
{
    var options = new DatasyncClientOptions
    {
        HttpPipeline = new DelegatingHandler[] { new MyHandler() }
    };
    var client = new Datasync("AppUrl", options);
    var todoTable = client.GetRemoveTable<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;
    }
}