This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

开始使用 Dapr 客户端 .NET SDK

如何使用 Dapr .NET SDK 快速上手

Dapr 客户端包使您能够从 .NET 应用程序与其他 Dapr 应用程序进行交互。

构建块

.NET SDK 允许您与所有 Dapr 构建块进行接口交互。

调用服务

HTTP

您可以使用 DaprClientSystem.Net.Http.HttpClient 来调用服务。

using var client = new DaprClientBuilder().
                UseTimeout(TimeSpan.FromSeconds(2)). // 可选:设置超时
                Build(); 

// 调用名为 "deposit" 的 POST 方法,输入类型为 "Transaction"
var data = new { id = "17", amount = 99m };
var account = await client.InvokeMethodAsync<object, Account>("routing", "deposit", data, cancellationToken);
Console.WriteLine("返回: id:{0} | 余额:{1}", account.Id, account.Balance);
var client = DaprClient.CreateInvokeHttpClient(appId: "routing");

// 设置 HTTP 客户端的超时:
client.Timeout = TimeSpan.FromSeconds(2);

var deposit = new Transaction  { Id = "17", Amount = 99m };
var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken);
var account = await response.Content.ReadFromJsonAsync<Account>(cancellationToken: cancellationToken);
Console.WriteLine("返回: id:{0} | 余额:{1}", account.Id, account.Balance);

gRPC

您可以使用 DaprClient 通过 gRPC 调用服务。

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint);
var client = new MyService.MyServiceClient(invoker);

var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1));
await client.MyMethodAsync(new Empty(), options);

Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode);

保存和获取应用程序状态

var client = new DaprClientBuilder().Build();

var state = new Widget() { Size = "small", Color = "yellow", };
await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken);
Console.WriteLine("状态已保存!");

state = await client.GetStateAsync<Widget>(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine($"获取状态: {state.Size} {state.Color}");

await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine("状态已删除!");

查询状态 (Alpha)

var query = "{" +
                "\"filter\": {" +
                    "\"EQ\": { \"value.Id\": \"1\" }" +
                "}," +
                "\"sort\": [" +
                    "{" +
                        "\"key\": \"value.Balance\"," +
                        "\"order\": \"DESC\"" +
                    "}" +
                "]" +
            "}";

var client = new DaprClientBuilder().Build();
var queryResponse = await client.QueryStateAsync<Account>("querystore", query, cancellationToken: cancellationToken);

Console.WriteLine($"获取 {queryResponse.Results.Count}");
foreach (var account in queryResponse.Results)
{
    Console.WriteLine($"账户: {account.Data.Id} 余额 {account.Data.Balance}");
}

发布消息

var client = new DaprClientBuilder().Build();

var eventData = new { Id = "17", Amount = 10m, };
await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken);
Console.WriteLine("已发布存款事件!");

与输出绑定交互

using var client = new DaprClientBuilder().Build();

// Twilio SendGrid 绑定的示例负载
var email = new 
{
    metadata = new 
    {
        emailTo = "customer@example.com",
        subject = "来自 Dapr SendGrid 绑定的邮件",    
    }, 
    data =  "<h1>测试 Dapr 绑定</h1>这是一个测试。<br>再见!",
};
await client.InvokeBindingAsync("send-email", "create", email);

检索秘密

var client = new DaprClientBuilder().Build();

// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"获取秘密键: {string.Join(", ", secrets.Keys)}");
var client = new DaprClientBuilder().Build();

// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"获取秘密键: {string.Join(", ", secrets.Keys)}");

// 检索单值秘密 - 返回一个 Dictionary<string, string>
// 包含一个以秘密名称为键的单个值
var data = await client.GetSecretAsync("mysecretstore", "single-value-secret");
var value = data["single-value-secret"]
Console.WriteLine("获取了一个秘密值,我不会打印它,因为它是秘密!");

获取配置键

var client = new DaprClientBuilder().Build();

// 检索特定的一组键。
var specificItems = await client.GetConfiguration("configstore", new List<string>() { "key1", "key2" });
Console.WriteLine($"这是我的值:\n{specificItems[0].Key} -> {specificItems[0].Value}\n{specificItems[1].Key} -> {specificItems[1].Value}");

// 通过提供一个空列表来检索所有配置项。
var specificItems = await client.GetConfiguration("configstore", new List<string>());
Console.WriteLine($"我得到了 {configItems.Count} 个条目!");
foreach (var item in configItems)
{
    Console.WriteLine($"{item.Key} -> {item.Value}")
}

订阅配置键

var client = new DaprClientBuilder().Build();

// 订阅配置 API 返回一个 IAsyncEnumerable<IEnumerable<ConfigurationItem>> 的包装器。
// 通过在 foreach 循环中访问其 Source 进行迭代。当流被切断或取消令牌被取消时,循环将结束。
var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token);
await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token))
{
    foreach (var item in items)
    {
        Console.WriteLine($"{item.Key} -> {item.Value}")
    }
}

分布式锁 (Alpha)

获取锁

using System;
using Dapr.Client;

namespace LockService
{
    class Program
    {
        [Obsolete("分布式锁 API 处于 Alpha 阶段,一旦稳定可以移除。")]
        static async Task Main(string[] args)
        {
            var daprLockName = "lockstore";
            var fileName = "my_file_name";
            var client = new DaprClientBuilder().Build();
     
            // 使用这种方法锁定也会自动解锁,因为这是一个可释放对象
            await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60))
            {
                if (fileLock.Success)
                {
                    Console.WriteLine("成功");
                }
                else
                {
                    Console.WriteLine($"锁定 {fileName} 失败。");
                }
            }
        }
    }
}

解锁现有锁

using System;
using Dapr.Client;

namespace LockService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var daprLockName = "lockstore";
            var client = new DaprClientBuilder().Build();

            var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
            Console.WriteLine(response.status);
        }
    }
}

管理工作流实例 (Alpha)

var daprClient = new DaprClientBuilder().Build();

string instanceId = "MyWorkflowInstance1";
string workflowComponentName = "dapr"; // 或者,这可以是 yaml 中定义的工作流组件的名称
string workflowName = "MyWorkflowDefinition";
var input = new { name = "Billy", age = 30 }; // 任何 JSON 可序列化的值都可以

// 启动工作流
var startResponse = await daprClient.StartWorkflowAsync(instanceId, workflowComponentName, workflowName, input);

// 终止工作流
await daprClient.TerminateWorkflowAsync(instanceId, workflowComponentName);

// 获取工作流元数据
var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponentName, workflowName);

Sidecar APIs

Sidecar 健康

.NET SDK 提供了一种轮询 sidecar 健康状态的方法,以及一个等待 sidecar 准备就绪的便捷方法。

轮询健康状态

当 sidecar 和您的应用程序都启动(完全初始化)时,此健康端点返回 true。

var client = new DaprClientBuilder().Build();

var isDaprReady = await client.CheckHealthAsync();

if (isDaprReady) 
{
    // 执行依赖 Dapr 的代码。
}

轮询健康状态(出站)

当 Dapr 初始化了其所有组件时,此健康端点返回 true,但可能尚未完成与您的应用程序的通信通道设置。

当您希望在启动路径中利用 Dapr 组件时,这种方法最好,例如,从 secretstore 加载秘密。

var client = new DaprClientBuilder().Build();

var isDaprComponentsReady = await client.CheckOutboundHealthAsync();

if (isDaprComponentsReady) 
{
    // 执行依赖 Dapr 组件的代码。
}

等待 sidecar

DaprClient 还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,建议包含一个 CancellationToken 以允许请求超时。以下是 DaprSecretStoreConfigurationProvider 中使用此方法的示例。

// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout))
{
    await client.WaitForSidecarAsync(tokenSource.Token);
}

// 在此处执行 Dapr 组件操作,例如获取秘密。

关闭 sidecar

var client = new DaprClientBuilder().Build();
await client.ShutdownSidecarAsync();

相关链接

1 - DaprClient 使用

使用 DaprClient 的基本提示和建议

生命周期管理

DaprClient 使用 TCP 套接字来访问网络资源,与 Dapr sidecar 进行通信。它实现了 IDisposable 接口,以便快速清理资源。

依赖注入

通过 AddDaprClient() 方法可以在 ASP.NET Core 中注册 Dapr 客户端。此方法接受一个可选的配置委托,用于配置 DaprClient,以及一个 ServiceLifetime 参数,允许您为注册的资源指定不同的生命周期,默认是 Singleton

以下示例展示了如何使用默认值注册 DaprClient

services.AddDaprClient();

您可以通过配置委托在 DaprClientBuilder 上指定选项来配置 DaprClient,例如:

services.AddDaprClient(daprBuilder => {
    daprBuilder.UseJsonSerializerOptions(new JsonSerializerOptions {
            WriteIndented = true,
            MaxDepth = 8
        });
    daprBuilder.UseTimeout(TimeSpan.FromSeconds(30));
});

另一个重载允许访问 DaprClientBuilderIServiceProvider,以便进行更高级的配置,例如从依赖注入容器中获取服务:

services.AddSingleton<SampleService>();
services.AddDaprClient((serviceProvider, daprBuilder) => {
    var sampleService = serviceProvider.GetRequiredService<SampleService>();
    var timeoutValue = sampleService.TimeoutOptions;
    
    daprBuilder.UseTimeout(timeoutValue);
});

手动实例化

除了依赖注入,您还可以使用静态客户端构建器手动创建 DaprClient

为了优化性能,建议创建一个长生命周期的 DaprClient 实例,并在整个应用程序中共享。DaprClient 是线程安全的,适合共享使用。

避免为每个操作创建一个新的 DaprClient 实例并在操作完成后释放它。

配置 DaprClient

在调用 .Build() 创建客户端之前,可以通过 DaprClientBuilder 类上的方法来配置 DaprClient。每个 DaprClient 对象的设置是独立的,创建后无法更改。

var daprClient = new DaprClientBuilder()
    .UseJsonSerializerSettings( ... ) // 配置 JSON 序列化器
    .Build();

默认情况下,DaprClientBuilder 会按以下顺序优先获取配置值:

  • 直接提供给 DaprClientBuilder 方法的值(例如 UseTimeout(TimeSpan.FromSeconds(30))
  • 从可选的 IConfiguration 中提取的值,与环境变量名称匹配
  • 从环境变量中提取的值
  • 默认值

DaprClientBuilder 上配置

DaprClientBuilder 提供以下方法来设置配置选项:

  • UseHttpEndpoint(string): 设置 Dapr sidecar 的 HTTP 端点
  • UseGrpcEndpoint(string): 设置 Dapr sidecar 的 gRPC 端点
  • UseGrpcChannelOptions(GrpcChannelOptions): 设置 gRPC 通道选项
  • UseHttpClientFactory(IHttpClientFactory): 配置 DaprClient 使用的 HttpClient 工厂
  • UseJsonSerializationOptions(JsonSerializerOptions): 配置 JSON 序列化
  • UseDaprApiToken(string): 为 Dapr sidecar 的身份验证提供令牌
  • UseTimeout(TimeSpan): 指定与 Dapr sidecar 通信时的超时值

IConfiguration 配置

除了直接从环境变量获取配置值,您还可以通过 IConfiguration 提供这些值。

例如,在多租户环境中,您可能需要为环境变量添加前缀。以下示例展示了如何从环境变量中获取这些值到 IConfiguration,并移除前缀:

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables("test_"); // 获取所有以 "test_" 开头的环境变量,并移除前缀
builder.Services.AddDaprClient();

从环境变量配置

SDK 会读取以下环境变量来配置默认值:

  • DAPR_HTTP_ENDPOINT: Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT: Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT: 如果未设置 DAPR_HTTP_ENDPOINT,则用于查找本地 HTTP 端点
  • DAPR_GRPC_PORT: 如果未设置 DAPR_GRPC_ENDPOINT,则用于查找本地 gRPC 端点
  • DAPR_API_TOKEN: 设置 API 令牌

配置 gRPC 通道选项

Dapr 使用 CancellationToken 进行取消,依赖于 gRPC 通道选项的配置,默认已启用。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置

var daprClient = new DaprClientBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
    .Build();

使用 DaprClient 进行取消

在 DaprClient 上执行异步操作的 API 接受一个可选的 CancellationToken 参数。这遵循 .NET 的标准惯例,用于可取消的操作。请注意,当取消发生时,不能保证远程端点停止处理请求,只能保证客户端已停止等待完成。

当操作被取消时,将抛出一个 OperationCancelledException

理解 DaprClient 的 JSON 序列化

DaprClient 上的许多方法使用 System.Text.Json 序列化器执行 JSON 序列化。接受应用程序数据类型作为参数的方法将对其进行 JSON 序列化,除非文档明确说明了其他情况。

如果您有高级需求,建议阅读 System.Text.Json 文档。Dapr .NET SDK 不提供独特的序列化行为或自定义 - 它依赖于底层序列化器将数据转换为和从应用程序的 .NET 类型。

DaprClient 被配置为使用从 JsonSerializerDefaults.Web 配置的序列化器选项对象。这意味着 DaprClient 将使用 camelCase 作为属性名称,允许读取带引号的数字("10.99"),并将不区分大小写地绑定属性。这些是与 ASP.NET Core 和 System.Text.Json.Http API 一起使用的相同设置,旨在遵循可互操作的 Web 约定。

截至 .NET 5.0,System.Text.Json 对所有 F# 语言特性内置支持不佳。如果您使用 F#,您可能需要使用一个添加对 F# 特性支持的转换器包,例如 FSharp.SystemTextJson

JSON 序列化的简单指导

如果您使用的功能集映射到 JSON 的类型系统,您在使用 JSON 序列化和 DaprClient 时的体验将会很顺利。这些是可以简化代码的通用指南。

  • 避免继承和多态
  • 不要尝试序列化具有循环引用的数据
  • 不要在构造函数或属性访问器中放置复杂或昂贵的逻辑
  • 使用与 JSON 类型(数值类型、字符串、DateTime)清晰映射的 .NET 类型
  • 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性
  • 设计具有 get/set 属性的类型,或者使用 支持的模式 用于 JSON 的不可变类型

多态性和序列化

DaprClient 使用的 System.Text.Json 序列化器在执行序列化时使用值的声明类型。

本节将使用 DaprClient.SaveStateAsync<TValue>(...) 作为示例,但建议适用于 SDK 暴露的任何 Dapr 构建块。

public class Widget
{
    public string Color { get; set; }
}
...

// 将 Widget 值作为 JSON 存储在状态存储中
Widget widget = new Widget() { Color = "Green", };
await client.SaveStateAsync("mystatestore", "mykey", widget);

在上面的示例中,类型参数 TValue 的类型参数是从 widget 变量的类型推断出来的。这很重要,因为 System.Text.Json 序列化器将根据值的声明类型执行序列化。结果是 JSON 值 { "color": "Green" } 将被存储。

考虑当您尝试使用 Widget 的派生类型时会发生什么:

public class Widget
{
    public string Color { get; set; }
}

public class SuperWidget : Widget
{
    public bool HasSelfCleaningFeature { get; set; }
}
...

// 将 SuperWidget 值作为 JSON 存储在状态存储中
Widget widget = new SuperWidget() { Color = "Green", HasSelfCleaningFeature = true, };
await client.SaveStateAsync("mystatestore", "mykey", widget);

在此示例中,我们使用了一个 SuperWidget,但变量的声明类型是 Widget。由于 JSON 序列化器的行为由声明类型决定,它只看到一个简单的 Widget,并将保存值 { "color": "Green" },而不是 { "color": "Green", "hasSelfCleaningFeature": true }

如果您希望 SuperWidget 的属性被序列化,那么最好的选择是用 object 覆盖类型参数。这将导致序列化器包含所有数据,因为它对类型一无所知。

Widget widget = new SuperWidget() { Color = "Green", HasSelfCleaningFeature = true, };
await client.SaveStateAsync<object>("mystatestore", "mykey", widget);

错误处理

当遇到故障时,DaprClient 上的方法将抛出 DaprException 或其子类。

try
{
    var widget = new Widget() { Color = "Green", };
    await client.SaveStateAsync("mystatestore", "mykey", widget);
}
catch (DaprException ex)
{
    // 处理异常,记录日志,重试等
}

最常见的故障情况将与以下内容相关:

  • Dapr 组件配置不正确
  • 瞬时故障,例如网络问题
  • 无效数据,例如 JSON 反序列化失败

在任何这些情况下,您都可以通过 .InnerException 属性检查更多异常详细信息。