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 客户端、服务器和虚拟 actor。

前置条件

安装

要开始使用 Client .NET SDK,请安装 Dapr .NET SDK 包:

dotnet add package Dapr.Client

试用

测试 Dapr .NET SDK。通过 .NET 快速入门和教程了解 Dapr 的实际应用:

SDK 示例描述
快速入门使用 .NET SDK 在几分钟内体验 Dapr 的 API 构建块。
SDK 示例克隆 SDK 仓库以尝试一些示例并开始使用。
发布订阅教程了解 Dapr .NET SDK 如何与其他 Dapr SDK 协作以实现发布订阅应用程序。

可用包

包名称文档链接描述
Dapr.Client文档创建与 Dapr 边车和其他 Dapr 应用程序交互的 .NET 客户端。
Dapr.AI文档在 .NET 中创建和管理 AI 操作。
Dapr.AI.A2a使用 A2A 框架实现 agent-to-agent 操作的 Dapr SDK。
Dapr.AI.Microsoft.Extensions文档通过 Dapr Conversation 构建块,以对话方式和使用工具轻松与 LLM 交互。
Dapr.AspNetCore文档使用 Dapr SDK 在 .NET 中编写服务器和服务。包括提供与 ASP.NET Core 更深度集成的支持和实用程序。
Dapr.Actors文档创建具有状态、提醒/定时器和方法的虚拟 actor。
Dapr.Actors.AspNetCore文档创建具有状态、提醒/定时器和方法的虚拟 actor,与 ASP.NET Core 深度集成。
Dapr.Actors.Analyzers文档一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Actors 时实现更好的实践并防止常见错误。
Dapr.Cryptography文档使用 Dapr 密码学构建块加密和解密任意大小的流状态。
Dapr.Jobs文档创建和管理作业的调度和编排。
Dapr.Jobs.Analyzers文档一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Jobs 时实现更好的实践并防止常见错误。
Dapr.DistributedLocks文档创建和管理分布式锁以管理独占资源访问。
Dapr.Extensions.Configuration用于 Microsoft.Extensions.Configuration 的 Dapr 密钥存储配置提供程序实现。
Dapr.PluggableComponents用于使用 .NET 实现 Dapr 可插拔组件。
Dapr.PluggableComponents.AspNetCore使用 .NET 实现 Dapr 可插拔组件,提供丰富的 ASP.NET Core 支持。
Dapr.PluggableComponents.Protos注意: 开发人员无需在其应用程序中直接安装此包。
Dapr.Messaging文档使用 Dapr Messaging SDK 构建分布式应用程序,该 SDK 利用流式发布订阅订阅等消息组件。
Dapr.Testcontainers文档使用基于 Testcontainers 的测试工具运行 Dapr 集成测试。
Dapr.Workflow文档创建和管理与其他 Dapr API 协作的工作流。
Dapr.Workflow.Versioning文档添加工作流版本控制策略以演进长时间运行的工作流。
Dapr.Workflow.Analyzers文档一组 Roslyn 源代码生成器和分析器,用于在 .NET 中使用 Dapr Workflows 时实现更好的实践并防止常见错误

更多信息

了解有关本地开发选项、最佳实践的更多信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。

1 - Dapr 客户端 .NET SDK 入门

如何开始使用 Dapr .NET SDK

Dapr 客户端包允许你从 .NET 应用程序与其他 Dapr 应用程序进行交互。

构建块

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

调用服务

HTTP

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

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();

using var scope = app.Services.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
 
// 调用名为 "deposit" 的 POST 方法,该方法接受类型为 "Transaction" 的输入
var data = new { id = "17", amount = 99m };
var account = await client.InvokeMethodAsync<Account>("routing", "deposit", data, cancellationToken);
Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance);

using Microsoft.Extensins.Hosting; using Microsoft.Extensions.DependencyInjection;

var builder = Host.CreateApplicationBuilder(args); builder.Services.AddDaprClient(); var app = builder.Build();

using var scope = app.Services.CreateScope(); var client = scope.ServiceProvider.GetRequiredService();

// 调用名为 “deposit” 的 POST 方法,该方法接受类型为 “Transaction” 的输入 var data = new { id = “17”, amount = 99m }; var account = await client.InvokeMethodAsync(“routing”, “deposit”, data, cancellationToken); Console.WriteLine(“Returned: id:{0} | Balance:{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("Returned: id:{0} | Balance:{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 state = new Widget() { Size = "small", Color = "yellow", };
await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken);
Console.WriteLine("Saved State!");

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

await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
Console.WriteLine("Deleted State!");

查询状态(Alpha)

var query = "{" +
                "\"filter\": {" +
                    "\"EQ\": { \"value.Id\": \"1\" }" +
                "}," +
                "\"sort\": [" +
                    "{" +
                        "\"key\": \"value.Balance\"," +
                        "\"order\": \"DESC\"" +
                    "}" +
                "]" +
            "}";
var queryResponse = await client.QueryStateAsync<Account>("querystore", query, cancellationToken: cancellationToken);

Console.WriteLine($"Got {queryResponse.Results.Count}");
foreach (var account in queryResponse.Results)
{
    Console.WriteLine($"Account: {account.Data.Id} has {account.Data.Balance}");
}

发布消息

var eventData = new { Id = "17", Amount = 10m, };
await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken);
Console.WriteLine("Published deposit event!");

与输出绑定交互

调用 InvokeBindingAsync 时,你可以选择自己处理序列化和编码,或者让 SDK 为你将其序列化为 JSON 然后编码为字节。

手动序列化

对于大多数场景,建议使用 InvokeBindingAsync 的此重载,因为它为你提供了清晰度和对数据处理方式的控制。

在此示例中,数据作为字符串的 UTF-8 字节表示发送。

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

var request = new BindingRequest("send-email", "create")
{
    // 注意:这是 Twilio SendGrid 绑定的示例负载 
    Data = Encoding.UTF8.GetBytes("<h1>Testing Dapr Bindings</h1>This is a test.<br>Bye!"),
    Metadata =
    {
        { "emailTo", "customer@example.com" },
        { "subject", "An email from Dapr SendGrid binding" },
    },
}
await client.InvokeBindingAsync(request);

自动序列化和编码

在此示例中,数据作为序列化为 JSON 的值的 UTF-8 编码字节表示发送。

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

var email = new 
{
    // 注意:这是 Twilio SendGrid 绑定的示例负载 
    data =  "<h1>Testing Dapr Bindings</h1>This is a test.<br>Bye!",
    metadata = new 
    {
        emailTo = "customer@example.com",
        subject = "An email from Dapr SendGrid binding",    
    },
};
await client.InvokeBindingAsync("send-email", "create", email);

检索机密

在检索机密之前,重要的是确保出站通道已注册并准备就绪,否则 SDK 将无法与 Dapr 边车进行双向通信。SDK 提供了一个用于此目的的辅助方法,称为 CheckOutboundHealthAsync。这不是指从 SDK 到运行时的出站,而是指从 Dapr 运行时回到使用 SDK 的客户端应用程序的出站。

此方法只是打开到 https://docs.dapr.io/zh-hans/reference/api/health_api/#wait-for-specific-health-check-against-outbound-path Dapr Health API 中的端点的连接,并评估返回的 HTTP 状态码以确定运行时报告的端点的健康状况。

重要的是要注意,WaitForSidecarAsync 方法和此方法执行几乎相同的操作;WaitForSidecarAsync 无限期轮询 CheckOutboundHealthAsync 端点,直到它返回健康状态值。它们仅用于机密或配置检索等情况。在其他场景中使用它们会导致意外行为(例如,端点永远无法准备就绪,因为没有注册使用"出站"通道的组件)。

此行为将在未来版本中更改,应谨慎依赖。

// 从 DI 获取 DaprClient 实例
var client = scope.GetRequiredService<DaprClient>();

// 等待出站通道建立 - 仅用于此场景,而非一般用途
await client.WaitForOutboundHealthAsync();

// 检索基于键值对的机密 - 返回 Dictionary<string, string>
var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secret");
Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}");
// 从 DI 获取 DaprClient 实例
var client = scope.GetRequiredService<DaprClient>();

// 等待出站通道建立 - 仅用于此场景,而非一般用途
await client.WaitForOutboundHealthAsync();

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

// 检索单值机密 - 返回 Dictionary<string, string>
// 包含一个以机密名称为键的单个值
var data = await client.GetSecretAsync("mysecretstore", "single-value-secret");
var value = data["single-value-secret"]
Console.WriteLine("Got a secret value, I'm not going to be print it, it's a secret!");

获取配置键

// 检索特定键集。
var specificItems = await client.GetConfiguration("configstore", new List<string>() { "key1", "key2" });
Console.WriteLine($"Here are my values:\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($"I got {configItems.Count} entires!");
foreach (var item in configItems)
{
    Console.WriteLine($"{item.Key} -> {item.Value}")
}

订阅配置键

// 订阅配置 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;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace LockService
{
    class Program
    {
        [Obsolete("Distributed Lock API is in Alpha, this can be removed once it's stable.")]
        static async Task Main(string[] args)
        {
            const string daprLockName = "lockstore";
            const string fileName = "my_file_name";
            
            var builder = Host.CreateDefaultBuilder();
            builder.ConfigureServices(services =>
            {
                services.AddDaprClient();
            });
            var app = builder.Build();
            
            using var scope = app.Services.CreateScope();
            var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
     
            // 使用此方法锁定也会自动解锁它,因为这是一个可释放对象
            await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60))
            {
                if (fileLock.Success)
                {
                    Console.WriteLine("Success");
                }
                else
                {
                    Console.WriteLine($"Failed to lock {fileName}.");
                }
            }
        }
    }
}

解锁现有锁

using System;
using Dapr.Client;

namespace LockService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var daprLockName = "lockstore";
            
            var builder = Host.CreateDefaultBuilder();
            builder.ConfigureServices(services =>
            {
                services.AddDaprClient();
            });
            var app = builder.Build();
            
            using var scope = app.Services.CreateScope();
            var client = scope.ServiceProvider.GetRequiredService<DaprClient>();
            
            var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123"));
            Console.WriteLine(response.status);
        }
    }
}

边车 API

边车健康检查

虽然 .NET SDK 提供了一种轮询边车健康状态的方法,但通常不建议开发人员使用此功能,除非他们明确使用 Dapr 来检索机密或配置值。

有两种方法可用:

“出站"方向是指从 Dapr 运行时到你的应用程序的出站通信。如果你的应用程序不使用 Actors、机密管理、配置检索或工作流,运行时将不会尝试创建出站连接。这意味着如果你的应用程序依赖于 WaitForSidecarAsync 而不使用任何这些 Dapr 组件,它将在启动期间无限期锁定,因为永远不会建立端点。

未来的版本将完全删除这些方法,并将其作为内部 SDK 操作执行,因此一般不应依赖任何一种方法。请在 Discord #dotnet-sdk 频道中联系以获取更多说明,了解你的场景是否可能需要使用此功能,但在大多数情况下,不应需要这些方法。

关闭边车

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

相关链接

1.1 - DaprClient 使用指南

使用 DaprClient 的基本技巧和建议

生命周期管理

DaprClient 持有对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。DaprClient 实现了 IDisposable 以支持资源的主动清理。

依赖注入

AddDaprClient() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入容器中。此方法接受一个可选的 options 委托来配置 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 对象的设置是独立的,在调用 .Build() 后无法更改。

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

默认情况下,DaprClientBuilder 将按以下顺序优先考虑以下位置来获取配置值:

  • 提供给 DaprClientBuilder 上某个方法的值(例如 UseTimeout(TimeSpan.FromSeconds(30))
  • 从可选注入的 IConfiguration 中提取的值,其名称与相关环境变量的预期名称匹配
  • 从相关环境变量中提取的值
  • 默认值

DaprClientBuilder 上配置

DaprClientBuilder 包含以下方法来设置配置选项:

  • UseHttpEndpoint(string):Dapr 边车的 HTTP 端点
  • UseGrpcEndpoint(string):设置 Dapr 边车的 gRPC 端点
  • UseGrpcChannelOptions(GrpcChannelOptions):设置用于连接到 Dapr 边车的 gRPC 通道选项
  • UseHttpClientFactory(IHttpClientFactory):配置 DaprClient 在构建 HttpClient 实例时使用已注册的 IHttpClientFactory
  • UseJsonSerializationOptions(JsonSerializerOptions):用于配置 JSON 序列化
  • UseDaprApiToken(string):将提供的令牌添加到每个请求以向 Dapr 边车进行身份验证
  • UseTimeout(TimeSpan):指定 HttpClient 与 Dapr 边车通信时使用的超时值

IConfiguration 配置

除了直接从环境变量获取配置值,或者因为值是从依赖注入的服务中获取的,另一种选择是使这些值在 IConfiguration 上可用。

例如,你可能要在多租户环境中注册应用程序,并且需要为所使用的环境变量添加前缀。以下示例展示了当这些环境变量的键以 test_ 为前缀时,如何将这些值从环境变量获取到你的 IConfiguration 中:

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

从环境变量配置

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

  • DAPR_HTTP_ENDPOINT:用于查找 Dapr 边车的 HTTP 端点,示例:https://dapr-api.mycompany.com
  • DAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,示例:https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT:如果未设置 DAPR_HTTP_ENDPOINT,则用于查找 Dapr 边车的 HTTP 本地端点,假定主机为 ‘127.0.0.1’
  • DAPR_GRPC_PORT:如果未设置 DAPR_GRPC_ENDPOINT,则用于查找 Dapr 边车的 gRPC 本地端点,假定主机为 ‘127.0.0.1’
  • DAPR_API_TOKEN:用于设置 API 令牌

作为一般规则,应同时指定 HTTP 和 gRPC 端口,或同时指定 HTTP 和 gRPC 端点。实际上,使用 HTTP 还是 gRPC 取决于你使用的具体 Dapr 服务以及 .NET SDK 是否支持 HTTP 或 gRPC 协议。随着时间的推移,绝大多数 .NET SDK 将专门支持 gRPC,但这永远不会适用于所有服务,因此为你的应用程序提供面向未来的保障并始终指定这两个值是更好的做法。

配置 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 类型的 .NET 类型(数值类型、字符串、DateTime
  • 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性
  • 使用具有 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 属性检查更多异常详细信息。

2 - Dapr Workflow .NET SDK

使用 Dapr Workflow 和 Dapr .NET SDK 快速上手

2.1 - DaprWorkflowClient 生命周期管理与注册

了解如何配置 DaprWorkflowClient 生命周期管理与依赖注入

生命周期管理

DaprWorkflowClient 掌控着用于与 Dapr 边车通信的 TCP 套接字形式持有的网络资源,以及其他用于工作流管理和操作的类型。DaprWorkflowClient 实现了 IAsyncDisposable 以支持资源的主动清理。

依赖注入

AddDaprWorkflow() 方法会将 Dapr 工作流服务注册到 ASP.NET Core 的依赖注入容器中。这个方法需要一个选项委托,用于定义你希望在应用中注册和使用的每个工作流和活动。

修改 gRPC 消息大小限制

在注册时,你还可以为工作流客户端配置 gRPC 消息大小限制。当工作流负载超出默认 gRPC 限制时,这会很有用。

services
    .AddDaprWorkflowClient()
    .WithGrpcMessageSizeLimits( 
        maxReceiveMessageSize: 16 * 1024 * 1024, 
        maxSendMessageSize: 16 * 1024 * 1024);

单例注册

默认情况下,AddDaprWorkflow 方法以单例生命周期注册 DaprWorkflowClient 及其相关服务。这意味着这些服务只会被实例化一次。

以下示例展示了如何在典型的 Program.cs 文件中注册 DaprWorkflowClient

builder.Services.AddDaprWorkflow(options => {
    options.RegisterWorkflow<YourWorkflow>();
    options.RegisterActivity<YourActivity>();
});

var app = builder.Build();
await app.RunAsync();

作用域注册

虽然默认行为在你的场景中通常是可以接受的,但你可能希望覆盖指定的生命周期。这可以通过在 AddDaprWorkflow 中传递 ServiceLifetime 参数来实现。例如,你可能希望在 ASP.NET Core 处理管道中注入另一个需要 DaprClient 所用上下文的作用域服务,如果该服务被注册为单例,这些上下文将无法获取。

以下示例展示了如何操作:

builder.Services.AddDaprWorkflow(options => {
    options.RegisterWorkflow<YourWorkflow>();
    options.RegisterActivity<YourActivity>();
}, ServiceLifecycle.Scoped);

var app = builder.Build();
await app.RunAsync();

瞬态注册

最后,Dapr 服务也可以注册为瞬态生命周期,这意味着每次注入时都会重新初始化。以下示例展示了如何操作:

builder.Services.AddDaprWorkflow(options => {
    options.RegisterWorkflow<YourWorkflow>();
    options.RegisterActivity<YourActivity>();
}, ServiceLifecycle.Transient);

var app = builder.Build();
await app.RunAsync();

创建 DaprWorkflowClient 实例

在 ASP.Net Core 应用中,你可以通过方法注入或构造函数注入将 DaprWorkflowClient 注入到方法或控制器中。本示例展示了在最小 API 场景下的方法注入:

app.MapPost("/start", async (
    [FromServices] DaprWorkflowClient daprWorkflowClient,
    Order order
    ) => {
        var instanceId = await daprWorkflowClient.ScheduleNewWorkflowAsync(
            nameof(OrderProcessingWorkflow),
            input: order);

        return Results.Accepted(instanceId);
});

要在控制台应用中创建 DaprWorkflowClient 实例,请从 ServiceProvider 中获取它:

using var scope = host.Services.CreateAsyncScope();
var daprWorkflowClient = scope.ServiceProvider.GetRequiredService<DaprWorkflowClient>();

现在,你可以使用此客户端执行工作流管理操作,例如启动、暂停、恢复和终止工作流实例。有关这些操作的更多信息,请参阅使用 DaprWorkflowClient 进行工作流管理操作

将服务注入到工作流活动中

工作流活动支持开发者对现代 C# 应用程序所期望的相同依赖注入方式。假设在启动时进行了正确的注册,任何此类类型都可以注入到工作流活动的构造函数中,并在工作流执行期间供其使用。这使得通过注入 ILogger 轻松添加日志记录,或通过注入 DaprClientDaprJobsClient 访问其他 Dapr 构建块变得简单。

internal sealed class SquareNumberActivity : WorkflowActivity<int, int>
{
    private readonly ILogger _logger;
    
    public MyActivity(ILogger logger)
    {
        this._logger = logger;
    }
    
    public override Task<int> RunAsync(WorkflowActivityContext context, int input) 
    {
        this._logger.LogInformation("Squaring the value {number}", input);
        var result = input * input;
        this._logger.LogInformation("Got a result of {squareResult}", result);
        
        return Task.FromResult(result);
    }
}

活动任务执行标识符

从 Dapr .NET SDK v1.17.0 开始,WorkflowActivityContext 暴露了一个任务执行标识符,该标识符具有以下特性:

  • 对每个活动任务唯一
  • 在重试期间保持稳定

这使得它可用于幂等键、任务级状态跟踪和日志关联。

internal sealed class IdempotentActivity : WorkflowActivity<int, int>
{
    public override Task<int> RunAsync(WorkflowActivityContext context, int input)
    {
        var executionId = context.TaskExecutionId;
        // 将 executionId 用作幂等键或任务状态键。

        return Task.FromResult(input * input);
    }
}

在工作流中使用 ILogger

由于工作流必须是确定性的,因此无法向其注入任意服务。例如,如果能够将标准 ILogger 注入到工作流中,并且由于错误需要重放工作流,那么从事件源日志进行的后续重放将导致日志记录额外的操作,这些操作实际上并没有发生第二次或第三次,因为它们的结果是从日志中获取的。这可能会引入大量的混淆。相反,提供了一个重放安全的记录器供在工作流内使用。它只会在工作流首次运行时记录事件,而在重放工作流时不会记录任何内容。

此记录器可以从工作流实例上可用的 WorkflowContext 中存在的方法获取,并且可以像使用 ILogger 实例一样精确使用。

.NET SDK 仓库中可以看到演示此功能的完整示例,但下面提供了该示例的简要摘录。

public class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
    public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
    {
        string orderId = context.InstanceId;
        var logger = context.CreateReplaySafeLogger<OrderProcessingWorkflow>(); // 使用此方法访问记录器实例

        logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost);
        
        //...
    }
}

后续步骤

2.2 - .NET SDK 中的工作流序列化

配置 Dapr .NET SDK 的工作流序列化

概述

从 Dapr .NET SDK v1.17.0 开始,Dapr.Workflow 支持可插拔序列化。SDK 默认继续使用 System.Text.Json,但现在您可以:

  • 覆盖默认的 System.Text.Json 设置。
  • 注册自定义序列化器(例如,MessagePack 或 BSON)。

序列化配置完全在客户端进行,不需要特定版本的 Dapr 运行时。

兼容性和重大变更

默认 JSON 序列化

默认情况下,.NET SDK 使用 System.Text.Json 并采用 JsonSerializerDefaults.Web(请参阅 JsonSerializerDefaults.Web 参考)。 这意味着:

  • 属性名称不区分大小写。
  • 属性名称使用 “camelCase” 格式。
  • 读取时允许带引号的数字(数字属性的 JSON 字符串)。

此默认约定旨在与其他 Dapr 语言 SDK 兼容,以支持多应用工作流。

覆盖 System.Text.Json 默认值

要覆盖默认 JSON 设置,请使用工作流构建器注册工作流客户端,以便您可以提供自定义的 JsonSerializerOptions

builder.Services
    .AddDaprWorkflowBuilder(options =>
    {
        options.RegisterWorkflow<MyWorkflow>();
        options.RegisterActivity<MyActivity>();
    })
    .WithJsonSerializer(new JsonSerializerOptions { PropertyNamingPolicy = null });

从 DI 解析的所有 DaprWorkflowClient 实例都将使用提供的 JsonSerializerOptions 来处理工作流和活动负载。

自定义序列化提供程序

自定义序列化器必须实现 IWorkflowSerializer 接口。以下示例展示了基于 MessagePack 的实现,该实现将数据编码为 Base64 字符串进行传输:

public sealed class MessagePackWorkflowSerializer : IWorkflowSerializer
{
    private readonly MessagePackSerializerOptions _options;

    public MessagePackWorkflowSerializer(MessagePackSerializerOptions options)
    {
        _options = options;
    }

    /// <inheritdoc/>
    public string Serialize(object? value, Type? inputType = null)
    {
        if (value == null)
        {
            return string.Empty;
        }

        var targetType = inputType ?? value.GetType();
        var bytes = MessagePackSerializer.Serialize(targetType, value, _options);
        return Convert.ToBase64String(bytes);
    }

    /// <inheritdoc/>
    public T? Deserialize<T>(string? data)
    {
        return (T?)Deserialize(data, typeof(T));
    }

    /// <inheritdoc/>
    public object? Deserialize(string? data, Type returnType)
    {
        if (returnType == null)
        {
            throw new ArgumentNullException(nameof(returnType));
        }

        if (string.IsNullOrEmpty(data))
        {
            return default;
        }

        try
        {
            var bytes = Convert.FromBase64String(data);
            return MessagePackSerializer.Deserialize(returnType, bytes, _options);
        }
        catch (FormatException ex)
        {
            throw new InvalidOperationException(
                "Failed to decode Base64 data. The input may not be valid MessagePack-serialized data.",
                ex);
        }
        catch (MessagePackSerializationException ex)
        {
            throw new InvalidOperationException(
                $"Failed to deserialize data to type {returnType.FullName}.",
                ex);
        }
    }
}

注册自定义序列化器

使用工作流构建器注册序列化器:

builder.Services
    .AddDaprWorkflowBuilder(options =>
    {
        options.RegisterWorkflow<MyWorkflow>();
        options.RegisterActivity<MyActivity>();
    })
    .WithSerializer(new MessagePackWorkflowSerializer(MessagePackSerializerOptions.Standard));

如果需要 DI 提供的配置,请使用接收 IServiceProvider 的重载:

builder.Services
    .AddDaprWorkflowBuilder(options =>
    {
        options.RegisterWorkflow<MyWorkflow>();
        options.RegisterActivity<MyActivity>();
    })
    .WithSerializer(serviceProvider =>
    {
        var options = serviceProvider
            .GetRequiredService<IOptions<MessagePackSerializerOptions>>()
            .Value;

        return new MessagePackWorkflowSerializer(options);
    });

2.3 - .NET SDK 中的多应用程序工作流

使用 .NET SDK 调用托管在其他 Dapr 应用程序中的活动和子工作流

概述

Dapr 工作流可以调用托管在不同 Dapr 应用程序中的活动或子工作流。在 .NET 中,多应用程序工作流的支持从以下版本开始:

  • Dapr 运行时 v1.16.0+
  • Dapr .NET SDK v1.17.0+

概念性指导和约束在 多应用程序工作流 中介绍。

要求

多应用程序工作流调用需要:

  • 目标应用 ID 必须存在,并且必须注册你调用的活动或工作流。
  • 所有参与的应用 ID 必须位于同一命名空间中。
  • 所有参与的应用 ID 必须使用相同的工作流(actor)状态存储。

在另一个应用程序中调用活动

在调用活动时,在 WorkflowTaskOptions 上设置 TargetAppId 以在另一个应用程序中执行它:

public sealed class BusinessWorkflow : Workflow<string, string>
{
    public override async Task<string> RunAsync(WorkflowContext context, string input)
    {
        var options = new WorkflowTaskOptions { TargetAppId = "App2" };
        var output = await context.CallActivityAsync<string>(nameof(ActivityA), input, options);
        return output;
    }
}

父工作流继续在本地进行编排并接收活动结果。

在另一个应用程序中调用子工作流

在调用子工作流时,在 ChildWorkflowTaskOptions 上设置 TargetAppId 以在另一个应用程序中执行它:

public sealed class BusinessWorkflow : Workflow<string, string>
{
    public override async Task<string> RunAsync(WorkflowContext context, string input)
    {
        var options = new ChildWorkflowTaskOptions { TargetAppId = "App2" };
        var output = await context.CallChildWorkflowAsync<string>(nameof(Workflow2), input, options);
        return output;
    }
}

后续步骤

2.4 - 使用 DaprWorkflowClient 进行工作流管理操作

了解如何使用 DaprWorkflowClient 管理工作流

使用 DaprWorkflowClient 进行工作流管理操作

DaprWorkflowClient 类提供了管理工作流实例的方法。以下是你可以使用 DaprWorkflowClient 执行的操作。

调度新的工作流实例

要启动新的工作流实例,请使用 ScheduleNewWorkflowAsync 方法。此方法需要工作流类型名称和工作流所需的输入参数。工作流的 instanceId 是一个可选参数;如果未提供,DaprWorkflowClient 会生成一个新的 GUID。最后一个可选参数是 DateTimeOffset 类型的 startTime,可用于定义工作流实例应该何时开始。该方法返回已调度工作流的 instanceId,用于其他工作流管理操作。

var instanceId = $"order-workflow-{Guid.NewGuid().ToString()[..8]}";
var input = new Order("Paperclips", 1000, 9.95);
await daprWorkflowClient.ScheduleNewWorkflowAsync(
  nameof(OrderProcessingWorkflow),
  instanceId,
  input);

获取工作流实例的状态

要获取工作流实例的当前状态,请使用 GetWorkflowStateAsync 方法。此方法需要工作流的实例 ID,并返回一个包含工作流当前状态详细信息的 WorkflowStatus 对象。

var workflowStatus = await daprWorkflowClient.GetWorkflowStateAsync(instanceId);

向正在运行的工作流实例发送事件

要向正在等待外部事件的运行中工作流实例发送事件,请使用 RaiseEventAsync 方法。此方法需要工作流的实例 ID、事件名称,以及可选的事件负载。

await daprWorkflowClient.RaiseEventAsync(instanceId, "Approval", true);

暂停正在运行的工作流实例

可以使用 SuspendWorkflowAsync 方法暂停正在运行的工作流实例。此方法需要工作流的实例 ID。你可以选择性地提供暂停工作流的原因。

await daprWorkflowClient.SuspendWorkflowAsync(instanceId);

恢复已暂停的工作流实例

可以使用 ResumeWorkflowAsync 方法恢复已暂停的工作流实例。此方法需要工作流的实例 ID。你可以选择性地提供恢复工作流的原因。

await daprWorkflowClient.ResumeWorkflowAsync(instanceId);

终止工作流实例

要终止工作流实例,请使用 TerminateWorkflowAsync 方法。此方法需要工作流的实例 ID。你可以选择性地提供一个 string 类型的 output 参数。终止工作流实例也会终止所有子工作流实例,但对正在执行的活动没有影响。

await daprWorkflowClient.TerminateWorkflowAsync(instanceId);

清除工作流实例

要从 Dapr Workflow 状态存储中删除工作流实例历史记录,请使用 PurgeWorkflowAsync 方法。此方法需要工作流的实例 ID。只有已完成、失败或已终止的工作流实例才能被清除。

await daprWorkflowClient.PurgeWorkflowAsync(instanceId);

后续步骤

2.5 - .NET SDK 中的工作流版本控制

了解如何在 Dapr .NET SDK 中使用基于补丁和基于名称的工作流版本控制

概述

Dapr 工作流版本控制允许你演进工作流,而不会中断进行中实例的确定性执行。 .NET SDK 支持两种方法:

  • 基于补丁的版本控制:引入由 context.IsPatched("patch-name") 守护的条件分支。
  • 基于名称的版本控制:创建一个新的工作流类型名称,并让版本控制策略选择最新版本。

对于小型就地更改,使用基于补丁的版本控制。对于需要全新的工作流类型的大型重构,使用基于名称的版本控制。

何时使用每种方法

基于补丁的版本控制 适用于以下情况:

  • 你需要在现有工作流中进行小型、增量式的更改。
  • 你希望现有实例在部署后保持确定性行为。
  • 你希望暂时避免引入新的工作流类型。

基于名称的版本控制 适用于以下情况:

  • 你想要一个没有累积补丁的全新工作流类型。
  • 你准备好删除旧的补丁块并更自由地重构。
  • 你希望基于命名约定自动选择版本。

基于补丁的版本控制

基于补丁的版本控制依赖于工作流内部的确定性开关。使用 WorkflowContext.IsPatched 来守护新行为:

public override async Task RunAsync(WorkflowContext context, OrderPayload input)
{
    await context.CallActivityAsync(nameof(ReserveInventoryActivity), input);

    if (context.IsPatched("v2"))
    {
        await context.CallActivityAsync(nameof(ChargePaymentActivityV2), input);
    }
    else
    {
        await context.CallActivityAsync(nameof(ChargePaymentActivity), input);
    }
}

补丁规则

  • 补丁名称可以在同一工作流中多次出现,并且可以嵌套。
  • 补丁名称在部署中必须唯一。例如,如果你部署了一个带有补丁名称 "v1" 的工作流,
  • 你不得在后续编辑中重复使用 "v1"。请使用新的标识符(如 "v2")以避免非确定性行为。
  • IsPatchedWorkflowContext 上可用;无需额外设置。

基于名称的版本控制

基于名称的版本控制允许你通过更改工作流类型名称来创建新版本的工作流。推荐的模式是将现有工作流复制到新文件中,重命名类,根据需要进行重构,然后在必要时再次开始打补丁。

例如,如果你有 OrderWorkflow,则创建 OrderWorkflowV2 并重构它。较旧的版本可以保留给进行中的实例,而新实例使用最新版本。

默认命名行为

默认情况下,基于名称的版本控制使用内置的 NumericVersionStrategy 和数字后缀。以下都是有效示例:

  • MyWorkflow(被视为版本 0
  • MyWorkflow2
  • MyWorkflowV2

默认策略假设较高的数字值是较新的版本(例如,MyWorkflowV10MyWorkflowV2 更新)。.NET SDK 还包含其他内置策略(Date、SemVer 和 Numeric)以及对自定义策略的支持。

内置策略和选项

.NET SDK 附带了几个内置的基于名称的策略。每个策略都支持允许你调整如何解析后缀以及当没有后缀时该如何处理的选项。

  • DateVersionStrategy:从尾随后缀派生基于日期的版本(例如,MyWorkflow20220611)。 选项包括:
    • 日期格式:使用标准 C# 日期格式规则;默认为 yyyyMMdd
    • 默认版本:当未提供后缀时使用;默认为 0
    • 前缀:在日期后缀之前匹配的可选前缀,带有可选区分大小写。
  • SemVerVersionStrategy:从尾随后缀派生 SemVer 版本(例如,MyWorkflow1.2.3)。 选项包括:
    • 前缀:在 SemVer 后缀之前匹配的可选前缀,带有可选区分大小写。
    • 预发布/生成支持:可以解析预发布注释和生成元数据。
    • 默认版本:当未提供后缀时的可选默认值(如果配置为允许缺少后缀)。
  • NumericVersionStrategy:从尾随后缀派生数字版本(例如,MyWorkflow42MyWorkflowV42)。 选项包括:
    • 前缀:在数字后缀之前匹配的可选前缀,带有可选区分大小写。
    • 零填充宽度:可选宽度,允许使用带前导零的固宽数字。
    • 默认版本:当未提供后缀时使用。

配置基于名称的版本控制

1. 安装版本控制包

Dapr.Workflow.Versioning 包添加到你的项目中。

2. 注册工作流版本控制

在启动期间将版本控制添加到 DI:

builder.Services.AddDaprWorkflowVersioning();

3. 选择策略(可选)

你可以在调用 AddDaprWorkflowVersioning 后在 DI 中注册策略来选择:

builder.Services.UseDefaultWorkflowStrategy<NumericVersionStrategy>("workflow-versioning-options");

可选字符串键用于定位策略选项。

4. 配置策略选项(可选)

使用相同的键注册策略选项:

builder.Services.ConfigureStrategyOptions<NumericVersionStrategyOptions>("workflow-versioning-options", o =>
{
    o.SuffixPrefix = "V";
});

5. 注册活动

活动照常注册。使用基于名称的版本控制时不需要注册工作流,因为源生成器会在构建时自动发现它们。

builder.Services.AddDaprWorkflow(w =>
{
    w.RegisterActivity<SendEmailActivity>();
});

配置完成后,命名工作流版本控制将在运行时自动应用。

跨程序集工作流发现

默认情况下,工作流版本控制源生成器仅扫描执行的程序集。如果你将工作流保留在单独的引用程序集中,则除非你选择加入引用扫描,否则不会发现这些实现。

默认情况下禁用引用扫描,因为它可能会增加构建时间(生成器必须检查所有引用的程序集以查找 Workflow<,> 实现)。要启用它,请将以下内容添加到执行应用程序的 .csproj 文件中:

<ItemGroup>
  <CompilerVisibleProperty Include="DaprWorkflowVersioningScanReferences" />
</ItemGroup>

启用后,源生成器会将引用程序集中发现的任何工作流实现添加到用于版本跟踪的内部注册表中。

覆盖名称和版本

如果你需要覆盖从工作流类型检测到的规范名称或版本,请将 [WorkflowVersion] 属性应用于实现工作流的类,并显式指定值。

最佳实践

  • 按规范名称调度:调度新的工作流实例时,使用规范工作流名称(未版本化的名称)。SDK 会自动发现版本化类型并将规范名称映射到最新版本。避免使用特定的版本化名称进行调度。
  • 保留旧版本:无限期保留较旧的工作流类型。只有在确定没有进行中的实例引用它们时才能删除它们。过早删除旧版本可能会导致长时间运行的工作流停滞。
  • 单向版本控制:始终向前推进版本。避免重命名或重复使用较旧的版本标识符。

当前限制

  • 单个项目不能为不同的工作流混合使用多个版本控制策略。
  • 没有从一个版本控制策略到另一个策略的自动迁移。
  • 工作流版本控制不能跨应用程序边界工作。如果你使用多应用工作流,必须根据目标应用的任何版本控制策略使用该应用期望的类型名称。调用此 .NET 应用程序的其他应用程序如果设置为使用基于名称的工作流版本控制,则可以简单地使用规范名称。

示例项目

端到端示例可在 Dapr .NET SDK 仓库的 examples/Workflow/WorkflowVersioning 中找到。

后续步骤

2.6 - .NET 工作流示例

探索 GitHub 上的 Dapr 工作流代码示例

Dapr Quickstarts 仓库中的工作流教程

GitHub 上的 Dapr Quickstarts 仓库包含许多工作流教程,展示各种工作流模式以及如何使用工作流管理操作。你可以在 quickstarts/tutorials/workflow/csharp 文件夹中找到这些教程。

.NET SDK 仓库中的工作流示例

GitHub 上的 Dapr .NET SDK 仓库包含多个示例,演示如何将 Dapr 工作流与 .NET 结合使用。你可以在 examples/Workflow 文件夹中找到这些示例。

后续步骤

3 - Dapr Actors .NET SDK

快速上手 Dapr Actors .NET SDK

通过 Dapr Actor 包,您可以从 .NET 应用程序与 Dapr 虚拟 Actor 进行交互。

要开始使用,请参阅 Dapr Actors 操作指南。

3.1 - IActorProxyFactory 接口

了解如何使用 IActorProxyFactory 接口创建 actor 客户端

Actor 类或 ASP.NET Core 项目中,建议使用 IActorProxyFactory 接口来创建 actor 客户端。

AddActors(...) 方法会将 actor 服务注册到 ASP.NET Core 依赖注入中。

  • 在 actor 实例外部: IActorProxyFactory 实例作为单例服务通过依赖注入可用。
  • 在 actor 实例内部: IActorProxyFactory 实例作为属性(this.ProxyFactory)可用。

以下是在 actor 内部创建代理的示例:

public Task<MyData> GetDataAsync()
{
    var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");
    await proxy.DoSomethingGreat();

    return this.StateManager.GetStateAsync<MyData>("my_data");
}

在本指南中,你将学习如何使用 IActorProxyFactory

标识 actor

IActorProxyFactory 上的所有 API 都需要 actor 类型 和 actor id 才能与 actor 通信。对于强类型客户端,你还需要它的接口之一。

  • Actor 类型 在整个应用程序中唯一标识 actor 实现。
  • Actor id 唯一标识该类型的一个实例。

如果你没有 actor id 并且想要与一个新实例通信,可以使用 ActorId.CreateRandom() 创建一个随机 id。由于随机 id 是一个加密强度高的标识符,当你与它交互时,运行时会创建一个新的 actor 实例。

你可以使用 ActorReference 类型与其他 actor 交换 actor 类型和 actor id,作为消息的一部分。

两种 actor 客户端风格

actor 客户端支持两种不同的调用风格:

Actor 客户端风格描述
强类型强类型客户端基于 .NET 接口,提供强类型的典型好处。它们不适用于非 .NET actor。
弱类型弱类型客户端使用 ActorProxy 类。建议仅在需要互操作性或其他高级原因时使用这些客户端。

使用强类型客户端

以下示例使用 CreateActorProxy<> 方法创建强类型客户端。CreateActorProxy<> 需要一个 actor 接口类型,并将返回该接口的一个实例。

// 为 IOtherActor 创建一个代理,类型为 OtherActor,具有随机 id
var proxy = this.ProxyFactory.CreateActorProxy<IOtherActor>(ActorId.CreateRandom(), "OtherActor");

// 调用接口定义的方法来调用 actor
//
// proxy 是 IOtherActor 的实现,所以我们可以直接调用其方法
await proxy.DoSomethingGreat();

使用弱类型客户端

以下示例使用 Create 方法创建弱类型客户端。Create 返回 ActorProxy 的一个实例。

// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");

// 按名称调用方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
await proxy.InvokeMethodAsync("DoSomethingGreat");

由于 ActorProxy 是弱类型代理,你需要将 actor 方法名称作为字符串传入。

你还可以使用 ActorProxy 调用具有请求和响应消息的方法。请求和响应消息将使用 System.Text.Json 序列化器进行序列化。

// 为类型 OtherActor 创建一个代理,具有随机 id
var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "OtherActor");

// 调用代理上的方法来调用 actor
//
// proxy 是 ActorProxy 的一个实例
var request = new MyRequest() { Message = "Hi, it's me.", };
var response = await proxy.InvokeMethodAsync<MyRequest, MyResponse>("DoSomethingGreat", request);

使用弱类型代理时,你_必须_主动定义正确的 actor 方法名称和消息类型。使用强类型代理时,这些名称和类型作为接口定义的一部分为你定义。

Actor 方法调用异常详细信息

actor 方法调用异常详细信息会向调用方和被调用方公开,提供追踪问题的入口点。异常详细信息包括:

  • 方法名称
  • 行号
  • 异常类型
  • UUID

你使用 UUID 在调用方和被调用方之间匹配异常。以下是异常详细信息的示例:

Dapr.Actors.ActorMethodInvocationException: Remote Actor Method Exception, DETAILS: Exception: NotImplementedException, Method Name: ExceptionExample, Line Number: 14, Exception uuid: d291a006-84d5-42c4-b39e-d6300e9ac38b

后续步骤

了解如何使用 ActorHost 编写和运行 actor

3.2 - Author & run actors

Learn all about authoring and running actors with the .NET SDK

创建 actor

ActorHost

ActorHost

  • 是所有 actor 必需的构造函数参数
  • 由运行时提供
  • 必须传递给基类构造函数
  • 包含允许该 actor 实例与运行时通信的所有状态
internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host) // Accept ActorHost in the constructor
        : base(host) // Pass ActorHost to the base class constructor
    {
    }
}

由于 ActorHost 包含 actor 独有的状态,你无需将实例传递到代码的其他部分。建议仅在测试中创建自己的 ActorHost 实例。

依赖注入

Actor 支持将额外参数依赖注入到构造函数中。你定义的任何其他参数都将从依赖注入容器中获取其值。

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, BankService bank) // Accept BankService in the constructor
        : base(host)
    {
        ...
    }
}

actor 类型应具有单个 public 构造函数。actor 基础设施使用 ActivatorUtilities 模式来构造 actor 实例。

你可以在 Startup.cs 中注册类型以使其可用于依赖注入。阅读更多关于注册类型的不同方式

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    ...

    // Register additional types with dependency injection.
    services.AddSingleton<BankService>();
}

每个 actor 实例都有自己的依赖注入作用域,并在执行操作后的段时间内保留在内存中。在此期间,与 actor 关联的依赖注入作用域也被视为活动状态。当 actor 被停用时,该作用域将被释放。

如果 actor 在构造函数中注入了 IServiceProvider,则 actor 将接收对其作用域关联的 IServiceProvider 的引用。IServiceProvider 可用于在未来动态解析服务。

internal class MyActor : Actor, IMyActor, IRemindable
{
    public MyActor(ActorHost host, IServiceProvider services) // Accept IServiceProvider in the constructor
        : base(host)
    {
        ...
    }
}

使用此模式时,避免创建许多实现 IDisposable瞬态服务实例。由于与 actor 关联的作用域可能在较长时间内被视为有效,因此可能会在内存中累积许多服务。有关更多信息,请参阅依赖注入指南

IDisposable 和 actor

Actor 可以实现 IDisposableIAsyncDisposable。建议依赖依赖注入进行资源管理,而不是在应用程序代码中实现 dispose 功能。在确实必要的罕见情况下才提供 dispose 支持。

日志记录

在 actor 类中,你可以通过基 Actor 类上的属性访问 ILogger 实例。此实例连接到 ASP.NET Core 日志系统,应用于 actor 内的所有日志记录。阅读更多关于日志记录。你可以配置多种不同的日志格式和输出接收器。

使用带有_命名占位符_的_结构化日志记录_,如下所示:

public Task<MyData> GetDataAsync()
{
    this.Logger.LogInformation("Getting state at {CurrentTime}", DateTime.UtcNow);
    return this.StateManager.GetStateAsync<MyData>("my_data");
}

记录日志时,避免使用格式字符串,如:$"Getting state at {DateTime.UtcNow}"

日志记录应使用命名占位符语法,它提供更好的性能和与日志系统的集成。

使用显式 actor 类型名称

默认情况下,客户端看到的 actor 类型_派生自 actor 实现类的_名称。默认名称将是类名(不带命名空间)。

如果需要,可以通过将 ActorAttribute 属性附加到 actor 实现类来指定显式类型名称。

[Actor(TypeName = "MyCustomActorTypeName")]
internal class MyActor : Actor, IMyActor
{
    // ...
}

在上面的示例中,名称将是 MyCustomActorTypeName

无需更改向运行时注册 actor 类型的代码,通过属性提供值就是所需的全部。

在服务器上托管 actor

注册 actor

Actor 注册是 Startup.csConfigureServices 的一部分。你可以通过 ConfigureServices 方法注册具有依赖注入的服务。注册 actor 类型集是 actor 服务注册的一部分。

ConfigureServices 内部,你可以:

  • 注册 actor 运行时(AddActors
  • 注册 actor 类型(options.Actors.RegisterActor<>
  • 配置 actor 运行时设置 options
  • 注册额外的服务类型以注入到 actor 的依赖注入中(services
// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Register actor runtime with DI
    services.AddActors(options =>
    {
        // Register actor types and configure actor settings
        options.Actors.RegisterActor<MyActor>();
        
        // Configure default settings
        options.ActorIdleTimeout = TimeSpan.FromMinutes(10);
        options.ActorScanInterval = TimeSpan.FromSeconds(35);
        options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(35);
        options.DrainRebalancedActors = true;
    });

    // Register additional services for use with actors
    services.AddSingleton<BankService>();
}

配置 JSON 选项

actor 运行时使用 System.Text.Json 进行:

  • 将数据序列化到状态存储
  • 处理来自弱类型客户端的请求

默认情况下,actor 运行时使用基于 JsonSerializerDefaults.Web 的设置。

你可以作为 ConfigureServices 的一部分配置 JsonSerializerOptions

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddActors(options =>
    {
        ...
        
        // Customize JSON options
        options.JsonSerializerOptions = ...
    });
}

Actor 和路由

ASP.NET Core 对 actor 的托管支持使用终结点路由系统。.NET SDK 不支持使用早期 ASP.NET Core 版本中的旧路由系统托管 actor。

由于 actor 使用终结点路由,actor HTTP 处理程序是中间件管道的一部分。以下是设置带有 actor 的中间件管道的 Configure 方法的最小示例。

// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Register actors handlers that interface with the Dapr runtime.
        endpoints.MapActorsHandlers();
    });
}

UseRoutingUseEndpoints 调用是配置路由所必需的。通过在终结点中间件内添加 MapActorsHandlers 将 actor 配置为管道的一部分。

这是一个最小示例,Actor 功能与以下功能并存是有效的:

  • Controllers
  • Razor Pages
  • Blazor
  • gRPC Services
  • Dapr pub/sub handler
  • 其他终结点,如运行状况检查

有问题的中间件

某些中间件可能会干扰 Dapr 请求到 actor 处理程序的路由。特别是,UseHttpsRedirection 对 Dapr 的默认配置有问题。Dapr 默认情况下通过未加密的 HTTP 发送请求,UseHttpsRedirection 中间件将阻止这些请求。此中间件目前不能与 Dapr 一起使用。

// in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // INVALID - this will block non-HTTPS requests
    app.UseHttpsRedirection();
    // INVALID - this will block non-HTTPS requests

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Register actors handlers that interface with the Dapr runtime.
        endpoints.MapActorsHandlers();
    });
}

后续步骤

尝试运行和使用虚拟 actor 示例

3.3 - .NET SDK 中的 Actor 序列化

在 .NET 中为远程和非远程 Actor 序列化类型所需的步骤

Actor 序列化

Dapr actor 包使您能够在 .NET 应用程序中使用弱类型或强类型客户端来使用 Dapr virtual actors。每种方式使用不同的序列化方法。本文档将回顾这些差异,并传达一些关键的基本规则,以便在任何一种情况下都能理解。

请注意,由于这些不同的序列化方法,不支持可互换地使用弱类型或强类型 actor 客户端。使用一个 Actor 客户端持久化的数据将无法使用另一个 Actor 客户端访问,因此重要的是选择一个并在整个应用程序中一致地使用它。

弱类型 Dapr Actor 客户端

在本节中,您将学习如何配置 C# 类型,以便在使用弱类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用基于字符串的方法名称,以及使用 System.Text.Json 序列化程序序列化的请求和响应负载。请注意,此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。

当使用弱类型 Dapr Actor 客户端从各种 actor 调用方法时,不需要独立序列化或反序列化方法负载,因为 SDK 将代表您透明地执行此操作。

客户端将针对您构建的 .NET 版本可用的最新版本的 System.Text.Json,并且序列化遵循相关 .NET 文档中提供的所有固有功能。

序列化程序将配置为使用 JsonSerializerOptions.Web 默认选项,除非使用自定义选项配置覆盖,这意味着应用以下内容:

  • 属性名称的反序列化以不区分大小写的方式执行
  • 属性名称的序列化使用 camel casing 执行,除非属性被 [JsonPropertyName] 属性覆盖
  • 反序列化将从数字和/或字符串值中读取数值

基本序列化

在以下示例中,我们展示了一个名为 Doodad 的简单类,但它也可以是一个记录。

public class Doodad
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:

{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}

覆盖序列化属性名称

可以通过将 [JsonPropertyName] 属性应用于所需的属性来覆盖默认属性名称。

通常,对于您持久化到 actor 状态的类型,这不是必需的,因为您不打算独立于 Dapr 相关功能读取或写入它们,但以下内容仅是为了清楚地说明这是可能的。

覆盖类上的属性名称

以下示例演示了使用 JsonPropertyName 更改序列化后第一个属性的名称。请注意,Count 属性上最后一次使用 JsonPropertyName 与预期序列化的名称相匹配。这主要是为了证明应用此属性不会产生任何负面影响——事实上,如果您稍后决定更改默认序列化选项但仍需要在更改之前一致地访问以前序列化的属性,这可能是更可取的,因为 JsonPropertyName 将覆盖这些选项。

public class Doodad
{
    [JsonPropertyName("identifier")]
    public Guid Id { get; set; }
    public string Name { get; set; }
    [JsonPropertyName("count")]
    public int Count { get; set; }
}

这将序列化为以下内容:

{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5}

覆盖记录上的属性名称

让我们尝试对 C# 12 或更高版本的记录执行相同的操作:

public record Thingy(string Name, [JsonPropertyName("count")] int Count); 

由于在主构造函数中传递的参数(在 C# 12 中引入)可以应用于记录中的属性或字段,因此在某些不明确的情况下,使用 [JsonPropertyName] 属性可能需要指定您打算将属性应用于属性而不是字段。如果有必要,您可以在主构造函数中这样指示:

public record Thingy(string Name, [property: JsonPropertyName("count")] int Count);

如果在不需要的情况下将 [property: ] 应用于 [JsonPropertyName] 属性,则不会对序列化或反序列化产生负面影响,因为操作将正常进行,就好像它是一个属性(通常情况下,如果没有这样标记)。

枚举类型

枚举(包括平面枚举)可以序列化为 JSON,但持久化的值可能会让您感到惊讶。同样,开发人员不应该独立于 Dapr 处理序列化数据,但以下信息可能至少有助于诊断为什么看似微小的版本迁移无法按预期工作。

采用以下 enum 类型提供一年中的各个季节:

public enum Season
{
    Spring,
    Summer,
    Fall,
    Winter
}

我们将继续使用一个单独的演示类型来引用我们的 Season,并同时演示它如何与记录一起使用:

public record Engagement(string Name, Season TimeOfYear);

给定以下初始化实例:

var myEngagement = new Engagement("Ski Trip", Season.Winter);

这将序列化为以下 JSON:

{"name":  "Ski Trip", "season":  3}

我们的 Season.Winter 值表示为 3 可能是出乎意料的,但这是因为序列化程序将自动使用枚举值的数字表示形式,第一个值从零开始,并递增每个可用附加值的数值。同样,如果正在进行迁移并且开发人员翻转了枚举的顺序,这将对您的解决方案产生重大更改,因为在反序列化时序列化的数值将指向不同的值。

相反,System.Text.Json 提供了一个 JsonConverter,它将选择使用基于字符串的值而不是数值。需要将 [JsonConverter] 属性应用于枚举类型本身以启用此功能,但随后将在任何引用枚举的下游序列化或反序列化操作中实现。

[JsonConverter(typeof(JsonStringEnumConverter<Season>))]
public enum Season
{
    Spring,
    Summer,
    Fall,
    Winter
}

使用上面 myEngagement 实例中的相同值,这将生成以下 JSON:

{"name":  "Ski Trip", "season":  "Winter"}

因此,可以移动枚举成员,而无需担心在反序列化期间引入错误。

自定义枚举值

System.Text.Json 序列化平台开箱即用,不支持使用 [EnumMember] 来允许您更改序列化或反序列化期间使用的枚举值,但在某些情况下,这可能很有用。同样,假设您负责重构解决方案,以便为各种枚举应用更好的名称。您使用了上面详述的 JsonStringEnumConverter<TType>,因此您将枚举的名称保存为值而不是数值,但如果您更改枚举名称,这将引入重大更改,因为该名称将不再与状态中的名称匹配。

请注意,如果您选择使用此方法,应该用 [EnumMember] 属性装饰所有枚举成员,以便为每个枚举值一致地应用值,而不是杂乱无章。没有任何东西会在构建或运行时验证这一点,但它被视为最佳实践操作。

在这种情况下,如何指定持久化的精确值,同时更改枚举成员的名称?使用自定义 JsonConverter 和扩展方法,该方法可以在提供的情况下从附加的 [EnumMember] 属性中提取值。将以下内容添加到您的解决方案中:

public sealed class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
    /// <summary>读取并将 JSON 转换为类型 <typeparamref name="T" />。</summary>
    /// <param name="reader">读取器。</param>
    /// <param name="typeToConvert">要转换的类型。</param>
    /// <param name="options">指定要使用的序列化选项的对象。</param>
    /// <returns>转换后的值。</returns>
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 从 JSON 读取器获取字符串值
        var value = reader.GetString();

        // 遍历所有枚举值
        foreach (var enumValue in Enum.GetValues<T>())
        {
            // 从 EnumMember 属性获取值(如果有)
            var enumMemberValue = GetValueFromEnumMember(enumValue);

            // 如果值匹配,则返回枚举值
            if (value == enumMemberValue)
            {
                return enumValue;
            }
        }

        // 如果未找到匹配项,则引发异常
        throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}");
    }

    /// <summary>将指定值写入 JSON。</summary>
    /// <param name="writer">要写入的写入器。</param>
    /// <param name="value">要转换为 JSON 的值。</param>
    /// <param name="options">指定要使用的序列化选项的对象。</param>
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        // 从 EnumMember 属性获取值(如果有)
        var enumMemberValue = GetValueFromEnumMember(value);

        // 将值写入 JSON 写入器
        writer.WriteStringValue(enumMemberValue);
    }

    private static string GetValueFromEnumMember(T value)
    {
        MemberInfo[] member = typeof(T).GetMember(value.ToString(), BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public);
        if (member.Length == 0)
            return value.ToString();
        object[] customAttributes = member.GetCustomAttributes(typeof(EnumMemberAttribute), false);
        if (customAttributes.Length != 0)
        {
            EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)customAttributes;
            if (enumMemberAttribute != null && enumMemberAttribute.Value != null)
                return enumMemberAttribute.Value;
        }
        return value.ToString();
    }
}

现在让我们添加一个示例枚举器。我们将设置一个值,该值使用每个枚举成员的小写版本来演示这一点。不要忘记用 JsonConverter 属性装饰枚举,并引用我们的自定义转换器,而不是上一节中使用的数字到字符串转换器。

[JsonConverter(typeof(EnumMemberJsonConverter<Season>))]
public enum Season
{
    [EnumMember(Value="spring")]
    Spring,
    [EnumMember(Value="summer")]
    Summer,
    [EnumMember(Value="fall")]
    Fall,
    [EnumMember(Value="winter")]
    Winter
}

让我们使用之前的示例记录。我们还将添加一个 [JsonPropertyName] 属性只是为了增强演示:

public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear);

最后,让我们初始化一个新的实例:

var myEngagement = new Engagement("Conference", Season.Fall);

这一次,序列化将考虑来自附加的 [EnumMember] 属性的值,为我们提供了一种重构应用程序的机制,而不需要对状态中现有枚举值进行复杂的版本控制方案。

{"event":  "Conference",  "season":  "fall"}

多态序列化

在 Dapr Actor 客户端中使用多态类型时,必须正确处理序列化和反序列化,以确保实例化适当的派生类型。多态序列化允许您序列化基类型的对象,同时保留特定的派生类型信息。

要启用多态反序列化,必须在基类型上使用 [JsonPolymorphic] 属性。此外,至关重要的是包含 [AllowOutOfOrderMetadataProperties] 属性,以确保元数据属性(如 $type)可以被 System.Text.Json 正确处理,即使它们不是 JSON 对象中的第一个属性。

示例

[JsonPolymorphic]
[AllowOutOfOrderMetadataProperties]
public abstract class SampleValueBase
{
    public string CommonProperty { get; set; }
}

public class DerivedSampleValue : SampleValueBase
{
    public string SpecificProperty { get; set; }
}

在此示例中,SampleValueBase 类标记了 [JsonPolymorphic][AllowOutOfOrderMetadataProperties] 属性。此设置确保 $type 元数据属性可以在反序列化期间正确识别和处理,无论它在 JSON 对象中的位置如何。

通过遵循此方法,您可以在 Dapr Actor 客户端中有效地管理多态序列化和反序列化,确保实例化和使用正确的派生类型。

强类型 Dapr Actor 客户端

在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时在运行时正确序列化和反序列化。这些客户端使用 .NET 接口实现,与使用其他语言编写的 Dapr Actor 兼容。

此 actor 客户端使用名为 Data Contract Serializer 的序列化引擎序列化数据,该引擎将 C# 类型转换为 XML 文档并从中转换。此序列化框架并非特定于 Dapr,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护。

发送或接收基元类型(如字符串或整数)时,此序列化会透明地发生,您无需进行任何必要的准备。但是,在处理您自己创建的复杂类型时,需要考虑一些重要规则,以便此过程顺利进行。

可序列化类型

使用 Data Contract Serializer 时,有几个重要的注意事项需要牢记:

  • 默认情况下,所有类型、读/写属性(构造后)和标记为公开可见的字段都会被序列化
  • 所有类型必须公开公共无参构造函数或使用 DataContractAttribute 属性进行装饰
  • 仅支持使用 DataContractAttribute 属性的 Init-only 设置器
  • 只读字段、没有 Get 和 Set 方法的属性以及具有私有 Get 和 Set 方法的内部或私有属性在序列化期间将被忽略
  • 支持对使用本身未标记 DataContractAttribute 属性的其他复杂类型的类型进行序列化,通过使用 KnownTypesAttribute 属性
  • 如果类型标记了 DataContractAttribute 属性,则您希望序列化和反序列化的所有成员也必须使用 DataMemberAttribute 属性进行装饰,否则将设置为其默认值

反序列化如何工作?

反序列化使用的方法取决于类型是否使用 DataContractAttribute 属性进行装饰。如果不存在此属性,则使用无参构造函数创建类型的实例。然后,每个属性和字段使用其各自的设置器映射到类型中,并将实例返回给调用者。

如果类型标记了 [DataContract],序列化程序将改为使用反射读取类型的元数据,并根据是否使用 DataMemberAttribute 属性标记来确定应包含哪些属性或字段,因为它是基于选择性加入的方式执行的。然后,它在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用 init-only 设置器。在此过程中,将根据情况调用序列化回调,然后将对象返回给调用者。

强烈建议使用序列化属性,因为它们提供了更大的灵活性来覆盖名称和命名空间,并且通常使用更多的现代 C# 功能。虽然默认序列化程序可以用于基元类型,但不建议将其用于您自己的任何类型,无论是类、结构体还是记录。如果您使用 DataContractAttribute 属性装饰类型,还建议显式使用 DataMemberAttribute 属性装饰要序列化或反序列化的每个成员。

.NET 类

类在 Data Contract Serializer 中得到完全支持,前提是还遵循本文档和 Data Contract Serializer 文档中详述的其他规则。

这里要记住的最重要的事情是,您必须拥有公共无参构造函数,或者必须使用适当的属性对其进行装饰。让我们查看一些示例来真正阐明什么可行,什么不可行。

在以下示例中,我们展示了一个名为 Doodad 的简单类。这里我们没有提供显式构造函数,因此编译器将提供默认的无参构造函数。因为我们使用的是 支持的基元类型(Guid、string 和 int32),并且我们的所有成员都具有公共 getter 和 setter,所以不需要任何属性,并且我们可以毫无问题地在 Dapr actor 方法中发送和接收此类时使用此类。

public class Doodad
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

默认情况下,这将使用类型中使用的成员名称以及其实例化的任何值进行序列化:

<Doodad>
  <Id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</Id>
  <Name>DoodadName</Name>
  <Count>5</Count>
</Doodad>

所以让我们调整它——让我们添加自己的构造函数,并且只在成员上使用 init-only 设置器。这将无法序列化和反序列化,不是因为使用了 init-only 设置器,而是因为没有无参构造函数。

// 无法正确序列化!
public class Doodad
{
    public Doodad(string name, int count)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    public Guid Id { get; set; }
    public string Name { get; init; }
    public int Count { get; init; }
}

如果我们向类型添加公共无参构造函数,我们就可以开始了,这将无需进一步注释即可工作。

public class Doodad
{
    public Doodad()
    {
    }

    public Doodad(string name, int count)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

但是如果我们不想添加这个构造函数呢?也许您不希望您的开发人员意外地使用非预期的构造函数创建此 Doodad 的实例。这就是更灵活的属性有用的地方。如果使用 DataContractAttribute 属性装饰类型,则可以删除无参构造函数,它将再次工作。

[DataContract]
public class Doodad
{
    public Doodad(string name, int count)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; }
}

在上面的示例中,我们不需要使用 DataMemberAttribute 属性,因为同样,我们使用的是序列化程序支持的 内置基元类型。但是,如果我们使用属性,我们会获得更多的灵活性。通过 DataContractAttribute 属性,我们可以使用 Namespace 参数指定我们自己的 XML 命名空间,并通过 Name 参数,在序列化到 XML 文档时更改类型的名称。

建议的做法是将 DataContractAttribute 属性附加到类型,并将 DataMemberAttribute 属性附加到要序列化的所有成员——如果它们不是必需的并且您没有更改默认值,它们将被忽略,但它们为您提供了一种选择序列化本来不会包含的成员的机制,例如标记为私有的成员或本身就是复杂类型或集合的成员。

请注意,如果您选择序列化私有成员,它们的值将被序列化为纯文本——根据您在序列化后处理数据的方式,它们很有可能被查看、拦截并可能被篡改,因此重要的是在您的用例中考虑是否要标记这些成员。

在以下示例中,我们将查看使用属性来更改某些成员的序列化名称,以及引入 IgnoreDataMemberAttribute 属性。顾名思义,这告诉序列化程序跳过此属性,即使它本来符合序列化条件。此外,因为我使用 DataContractAttribute 属性装饰类型,这意味着我可以在属性上使用 init-only 设置器。

[DataContract(Name="Doodad")]
public class Doodad
{
    public Doodad(string name = "MyDoodad", int count = 5)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    [DataMember(Name = "id")]
    public Guid Id { get; init; }
    [IgnoreDataMember]
    public string Name { get; init; }
    [DataMember]
    public int Count { get; init; }
}

序列化时,因为我们更改了序列化成员的名称,我们可以期望使用默认值的新 Doodad 实例序列化为:

<Doodad>
  <id>a06ced64-4f42-48ad-84dd-46ae6a7e333d</id>
  <Count>5</Count>
</Doodad>
C# 12 中的类 - 主构造函数

C# 12 为我们带来了类的主构造函数。使用主构造函数意味着编译器将被阻止创建默认的隐式无参构造函数。虽然类上的主构造函数不会生成任何公共属性,但这意味着如果向此主构造函数传递任何参数或在类中有非基元类型,则要么需要指定自己的无参构造函数,要么使用序列化属性。

这是一个示例,我们使用主构造函数将 ILogger 注入到字段并添加我们自己的无参构造函数,而无需任何属性。

public class Doodad(ILogger<Doodad> _logger)
{
    public Doodad() {} // 我们的无参构造函数

    public Doodad(string name, int count)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Count { get; set; } 
}

并使用我们的序列化属性(同样,因为我们使用序列化属性,所以选择使用 init-only 设置器):

[DataContract]
public class Doodad(ILogger<Doodad> _logger)
{
    public Doodad(string name, int count)
    {
        Id = Guid.NewGuid();
        Name = name;
        Count = count;
    }

    [DataMember]
    public Guid Id { get; init; }
    [DataMember]
    public string Name { get; init; }
    [DataMember]
    public int Count { get; init; }
}

.NET 结构体

结构体由 Data Contract 序列化程序支持,前提是它们使用 DataContractAttribute 属性标记,并且您希望序列化的成员使用 DataMemberAttribute 属性标记。此外,为了支持反序列化,结构体还需要具有无参构造函数。即使在 C# 10 中定义了自己的无参构造函数,这也可以工作。

[DataContract]
public struct Doodad
{
    [DataMember]
    public int Count { get; set; }
}

.NET 记录

记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该使用 DataContractAttribute 属性装饰所有记录,并使用 DataMemberAttribute 属性装饰要序列化的成员,以免在使用此或其他较新的 C# 功能时遇到任何反序列化问题。因为记录类默认对属性使用 init-only 设置器并鼓励使用主构造函数,所以将这些属性应用于类型可确保序列化程序能够正确地容纳您的类型。

通常,记录使用新的主构造函数概念呈现为简单的单行语句:

public record Doodad(Guid Id, string Name, int Count);

一旦您在 Dapr actor 方法调用中使用它,这将抛出一个错误,鼓励使用序列化属性,因为没有可用的无参构造函数,也没有使用上述属性进行装饰。

这里我们添加一个显式的无参构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用 init-only 设置器创建的。因为它没有对任何成员使用 DataContractAttribute 属性或 DataMemberAttribute 属性,所以序列化程序将无法在反序列化期间正确映射目标成员。

public record Doodad(Guid Id, string Name, int Count)
{
    public Doodad() {}
}

这种方法不需要额外的构造函数,而是依赖于序列化属性。因为我们使用 DataContractAttribute 属性标记类型,并使用自己的 DataMemberAttribute 属性装饰每个成员,所以序列化引擎将能够毫无问题地从 XML 文档映射到我们的类型。

[DataContract]
public record Doodad(
        [property: DataMember] Guid Id,
        [property: DataMember] string Name,
        [property: DataMember] int Count)

支持的基元类型

.NET 中内置了多种类型,这些类型被视为基元类型,无需开发人员付出额外努力即可进行序列化:

还有其他不是真正基元类型但具有类似内置支持的类型:

同样,如果您想通过 actor 方法传递这些类型,无需额外考虑,因为它们将毫无问题地序列化和反序列化。此外,本身标记了 (SerializeableAttribute)[https://learn.microsoft.com/dotnet/api/system.serializableattribute] 属性的类型也将被序列化。

枚举类型

枚举(包括标志枚举)如果适当标记则是可序列化的。您希望序列化的枚举成员必须使用 EnumMemberAttribute 属性标记才能进行序列化。在此属性的可选 Value 参数中传递自定义值将允许您指定成员在序列化文档中使用的值,而不是让序列化程序从成员名称派生它。

枚举类型不要求使用 DataContractAttribute 属性装饰类型——只需要您希望序列化的成员使用 EnumMemberAttribute 属性标记即可。

public enum Colors
{
    [EnumMember]
    Red,
    [EnumMember(Value="g")]
    Green,
    Blue, // 即使被类型使用,此值也不会被序列化,因为它没有使用 EnumMember 属性装饰
}

集合类型

关于数据约定序列化程序,所有实现 IEnumerable 接口的集合类型(包括数组和泛型集合)都被视为集合。实现 IDictionary 或泛型 IDictionary<TKey, TValue> 的类型被视为字典集合;所有其他类型都被视为列表集合。

与其他复杂类型一样,集合类型必须具有可用的无参构造函数。此外,它们还必须具有名为 Add 的方法,以便可以正确序列化和反序列化。这些集合类型使用的类型本身必须标记 DataContractAttribute 属性或按照本文档中的描述可序列化。

数据约定版本控制

由于数据约定序列化程序仅在 Dapr 中用于通过代理方法序列化和反序列化 .NET SDK 中的值与 Dapr actor 实例之间,因此几乎不需要考虑数据约定的版本控制,因为数据不会在使用相同序列化程序的应用程序版本之间持久化。对于那些有兴趣了解有关数据约定版本控制的更多信息的人,请访问此处

已知类型

通过使用 DataContractAttribute 属性标记每个类型,可以轻松嵌套您自己的复杂类型。这会通知序列化程序应如何执行反序列化。

但是,如果您正在处理多态类型,并且您的成员之一是具有派生类或其他实现的基类或接口呢?在这里,您将使用 KnownTypeAttribute 属性向序列化程序提供有关如何继续的提示。

当您将 KnownTypeAttribute 属性应用于类型时,您是在通知数据约定序列化程序它可能遇到哪些子类型,允许它正确处理这些类型的序列化和反序列化,即使运行时的实际类型与声明的类型不同。

[DataContract]
[KnownType(typeof(DerivedClass))]
public class BaseClass
{
    // 基类的成员
}

[DataContract]
public class DerivedClass : BaseClass 
{
    // 派生类的其他成员
}

在此示例中,BaseClass 标记了 [KnownType(typeof(DerivedClass))],这告诉数据约定序列化程序 DerivedClass 是它可能需要序列化或反序列化的 BaseClass 的可能实现。如果没有此属性,序列化程序在遇到实际上是 DerivedClass 类型的 BaseClass 实例时将不知道 DerivedClass,这可能会导致序列化异常,因为序列化程序将不知道如何处理派生类型。通过将所有可能的派生类型指定为已知类型,可以确保序列化程序可以正确处理该类型及其成员。

有关使用 [KnownType] 的更多信息和示例,请参阅官方文档

3.4 - 如何:在 .NET SDK 中运行和使用虚拟 Actor

通过此示例试用 .NET Dapr 虚拟 Actor

Dapr Actor 包允许你从 .NET 应用程序与 Dapr 虚拟 Actor 交互。在本指南中,你将学习如何:

  • 创建一个 Actor(MyActor)。
  • 在客户端应用程序上调用其方法。
MyActor --- MyActor.Interfaces
         |
         +- MyActorService
         |
         +- MyActorClient

接口项目 (\MyActor\MyActor.Interfaces)

该项目包含 actor 的接口定义。Actor 接口可以在任何名称的项目中定义。接口定义了由以下各方共享的 actor 契约:

  • actor 实现
  • 调用 actor 的客户端

由于客户端项目可能依赖它,最好在独立于 actor 实现的程序集中定义它。

Actor 服务项目 (\MyActor\MyActorService)

该项目实现承载 actor 的 ASP.Net Core Web 服务。它包含 actor 的实现 MyActor.cs。Actor 实现是一个类,它:

  • 派生自基类型 Actor
  • 实现 MyActor.Interfaces 项目中定义的接口。

Actor 类还必须实现一个构造函数,该构造函数接受一个 ActorService 实例和一个 ActorId,并将它们传递给基 Actor 类。

Actor 客户端项目 (\MyActor\MyActorClient)

该项目包含 actor 客户端的实现,该客户端调用 MyActor 的在 Actor Interfaces 中定义的方法。

前置条件

步骤 0:准备

由于我们将创建 3 个项目,请选择一个空目录作为起点,并在你选择的终端中打开它。

步骤 1:创建 actor 接口

Actor 接口定义了由 actor 实现和调用 actor 的客户端共享的 actor 契约。

Actor 接口定义需满足以下要求:

  • Actor 接口必须继承 Dapr.Actors.IActor 接口
  • Actor 方法的返回类型必须是 TaskTask<object>
  • Actor 方法最多只能有一个参数

创建接口项目并添加依赖项

# Create Actor Interfaces
dotnet new classlib -o MyActor.Interfaces

cd MyActor.Interfaces

# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors

cd ..

实现 IMyActor 接口

定义 IMyActor 接口和 MyData 数据对象。将以下代码粘贴到 MyActor.Interfaces 项目的 MyActor.cs 中。

using Dapr.Actors;
using Dapr.Actors.Runtime;
using System.Threading.Tasks;

namespace MyActor.Interfaces
{
    public interface IMyActor : IActor
    {       
        Task<string> SetDataAsync(MyData data);
        Task<MyData> GetDataAsync();
        Task RegisterReminder();
        Task UnregisterReminder();
        Task<IActorReminder> GetReminder();
        Task RegisterTimer();
        Task UnregisterTimer();
    }

    public class MyData
    {
        public string PropertyA { get; set; }
        public string PropertyB { get; set; }

        public override string ToString()
        {
            var propAValue = this.PropertyA == null ? "null" : this.PropertyA;
            var propBValue = this.PropertyB == null ? "null" : this.PropertyB;
            return $"PropertyA: {propAValue}, PropertyB: {propBValue}";
        }
    }
}

步骤 2:创建 actor 服务

Dapr 使用 ASP.NET Web 服务来承载 Actor 服务。本节将实现 IMyActor actor 接口并将 Actor 注册到 Dapr Runtime。

创建 actor 服务项目并添加依赖项

# Create ASP.Net Web service to host Dapr actor
dotnet new web -o MyActorService

cd MyActorService

# Add Dapr.Actors.AspNetCore nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors.AspNetCore

# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj

cd ..

添加 actor 实现

实现 IMyActor 接口并派生自 Dapr.Actors.Actor 类。以下示例还展示了如何使用 Actor Reminders。要使 Actors 使用 Reminders,它必须派生自 IRemindable。如果你不打算使用 Reminder 功能,可以跳过实现 IRemindable 和下面代码中显示的 reminder 特定方法。

将以下代码粘贴到 MyActorService 项目的 MyActor.cs 中:

using Dapr.Actors;
using Dapr.Actors.Runtime;
using MyActor.Interfaces;
using System;
using System.Threading.Tasks;

namespace MyActorService
{
    internal class MyActor : Actor, IMyActor, IRemindable
    {
        // The constructor must accept ActorHost as a parameter, and can also accept additional
        // parameters that will be retrieved from the dependency injection container
        //
        /// <summary>
        /// Initializes a new instance of MyActor
        /// </summary>
        /// <param name="host">The Dapr.Actors.Runtime.ActorHost that will host this actor instance.</param>
        public MyActor(ActorHost host)
            : base(host)
        {
        }

        /// <summary>
        /// This method is called whenever an actor is activated.
        /// An actor is activated the first time any of its methods are invoked.
        /// </summary>
        protected override Task OnActivateAsync()
        {
            // Provides opportunity to perform some optional setup.
            Console.WriteLine($"Activating actor id: {this.Id}");
            return Task.CompletedTask;
        }

        /// <summary>
        /// This method is called whenever an actor is deactivated after a period of inactivity.
        /// </summary>
        protected override Task OnDeactivateAsync()
        {
            // Provides Opporunity to perform optional cleanup.
            Console.WriteLine($"Deactivating actor id: {this.Id}");
            return Task.CompletedTask;
        }

        /// <summary>
        /// Set MyData into actor's private state store
        /// </summary>
        /// <param name="data">the user-defined MyData which will be stored into state store as "my_data" state</param>
        public async Task<string> SetDataAsync(MyData data)
        {
            // Data is saved to configured state store implicitly after each method execution by Actor's runtime.
            // Data can also be saved explicitly by calling this.StateManager.SaveStateAsync();
            // State to be saved must be DataContract serializable.
            await this.StateManager.SetStateAsync<MyData>(
                "my_data",  // state name
                data);      // data saved for the named state "my_data"

            return "Success";
        }

        /// <summary>
        /// Get MyData from actor's private state store
        /// </summary>
        /// <return>the user-defined MyData which is stored into state store as "my_data" state</return>
        public Task<MyData> GetDataAsync()
        {
            // Gets state from the state store.
            return this.StateManager.GetStateAsync<MyData>("my_data");
        }

        /// <summary>
        /// Register MyReminder reminder with the actor
        /// </summary>
        public async Task RegisterReminder()
        {
            await this.RegisterReminderAsync(
                "MyReminder",              // The name of the reminder
                null,                      // User state passed to IRemindable.ReceiveReminderAsync()
                TimeSpan.FromSeconds(5),   // Time to delay before invoking the reminder for the first time
                TimeSpan.FromSeconds(5));  // Time interval between reminder invocations after the first invocation
        }

        /// <summary>
        /// Get MyReminder reminder details with the actor
        /// </summary>
        public async Task<IActorReminder> GetReminder()
        {
            await this.GetReminderAsync("MyReminder");
        }

        /// <summary>
        /// Unregister MyReminder reminder with the actor
        /// </summary>
        public Task UnregisterReminder()
        {
            Console.WriteLine("Unregistering MyReminder...");
            return this.UnregisterReminderAsync("MyReminder");
        }

        // <summary>
        // Implement IRemindeable.ReceiveReminderAsync() which is call back invoked when an actor reminder is triggered.
        // </summary>
        public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
        {
            Console.WriteLine("ReceiveReminderAsync is called!");
            return Task.CompletedTask;
        }

        /// <summary>
        /// Register MyTimer timer with the actor
        /// </summary>
        public Task RegisterTimer()
        {
            return this.RegisterTimerAsync(
                "MyTimer",                  // The name of the timer
                nameof(this.OnTimerCallBack),       // Timer callback
                null,                       // User state passed to OnTimerCallback()
                TimeSpan.FromSeconds(5),    // Time to delay before the async callback is first invoked
                TimeSpan.FromSeconds(5));   // Time interval between invocations of the async callback
        }

        /// <summary>
        /// Unregister MyTimer timer with the actor
        /// </summary>
        public Task UnregisterTimer()
        {
            Console.WriteLine("Unregistering MyTimer...");
            return this.UnregisterTimerAsync("MyTimer");
        }

        /// <summary>
        /// Timer callback once timer is expired
        /// </summary>
        private Task OnTimerCallBack(byte[] data)
        {
            Console.WriteLine("OnTimerCallBack is called!");
            return Task.CompletedTask;
        }
    }
}

在 ASP.NET Core 启动时注册 actor 运行时

Actor 运行时通过 ASP.NET Core Startup.cs 进行配置。

运行时使用 ASP.NET Core 依赖注入系统来注册 actor 类型和基本服务。此集成通过 ConfigureServices(...) 中的 AddActors(...) 方法调用提供。使用传递给 AddActors(...) 的委托来注册 actor 类型并配置 actor 运行时设置。你可以在 ConfigureServices(...) 内部注册其他类型以进行依赖注入。这些类型将可用于注入到 Actor 类型的构造函数中。

Actors 通过与 Dapr 运行时的 HTTP 调用来实现。此功能是应用程序 HTTP 处理管道的一部分,并在 Configure(...) 内部的 UseEndpoints(...) 中注册。

将以下代码粘贴到 MyActorService 项目的 Startup.cs 中:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyActorService
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddActors(options =>
            {
                // Register actor types and configure actor settings
                options.Actors.RegisterActor<MyActor>();
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            // Register actors handlers that interface with the Dapr runtime.
            app.MapActorsHandlers();
        }
    }
}

步骤 3:添加客户端

创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 Actor Proxy 客户端来调用 Actor Interface 中定义的 actor 方法。

创建 actor 客户端项目并添加依赖项

# Create Actor's Client
dotnet new console -o MyActorClient

cd MyActorClient

# Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
dotnet add package Dapr.Actors

# Add Actor Interface reference
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj

cd ..

使用强类型客户端调用 actor 方法

你可以使用 ActorProxy.Create<IMyActor>(..) 创建强类型客户端并调用 actor 上的方法。

将以下代码粘贴到 MyActorClient 项目的 Program.cs 中:

using System;
using System.Threading.Tasks;
using Dapr.Actors;
using Dapr.Actors.Client;
using MyActor.Interfaces;

namespace MyActorClient
{
    class Program
    {
        static async Task MainAsync(string[] args)
        {
            Console.WriteLine("Startup up...");

            // Registered Actor Type in Actor Service
            var actorType = "MyActor";

            // An ActorId uniquely identifies an actor instance
            // If the actor matching this id does not exist, it will be created
            var actorId = new ActorId("1");

            // Create the local proxy by using the same interface that the service implements.
            //
            // You need to provide the type and id so the actor can be located. 
            var proxy = ActorProxy.Create<IMyActor>(actorId, actorType);

            // Now you can use the actor interface to call the actor's methods.
            Console.WriteLine($"Calling SetDataAsync on {actorType}:{actorId}...");
            var response = await proxy.SetDataAsync(new MyData()
            {
                PropertyA = "ValueA",
                PropertyB = "ValueB",
            });
            Console.WriteLine($"Got response: {response}");

            Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}...");
            var savedData = await proxy.GetDataAsync();
            Console.WriteLine($"Got response: {savedData}");
        }
    }
}

运行代码

你现在可以测试你创建的项目。

  1. 运行 MyActorService

    由于 MyActorService 承载着 actors,它需要使用 Dapr CLI 运行。

    cd MyActorService
    dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
    

    你将在此终端中看到来自 daprdMyActorService 的命令行输出。你应该会看到类似以下内容,这表明应用程序已成功启动。

    ...
    ℹ️  Updating metadata for app command: dotnet run
    ✅  You're up and running! Both Dapr and your app logs will appear here.
    
    == APP == info: Microsoft.Hosting.Lifetime[0]
    
    == APP ==       Now listening on: https://localhost:5001
    
    == APP == info: Microsoft.Hosting.Lifetime[0]
    
    == APP ==       Now listening on: http://localhost:5000
    
    == APP == info: Microsoft.Hosting.Lifetime[0]
    
    == APP ==       Application started. Press Ctrl+C to shut down.
    
    == APP == info: Microsoft.Hosting.Lifetime[0]
    
    == APP ==       Hosting environment: Development
    
    == APP == info: Microsoft.Hosting.Lifetime[0]
    
    == APP ==       Content root path: /Users/ryan/actortest/MyActorService
    
  2. 运行 MyActorClient

    MyActorClient 充当客户端,可以使用 dotnet run 正常运行。

    打开一个新终端并导航到 MyActorClient 目录。然后使用以下命令运行项目:

    dotnet run
    

    你应该会看到类似以下的命令行输出:

    Startup up...
    Calling SetDataAsync on MyActor:1...
    Got response: Success
    Calling GetDataAsync on MyActor:1...
    Got response: PropertyA: ValueA, PropertyB: ValueB
    

💡 此示例依赖于一些假设。ASP.NET Core Web 项目的默认监听端口为 5000,该端口作为 --app-port 5000 传递给 dapr run。Dapr sidecar 的默认 HTTP 端口为 3500。我们告诉 MyActorService 的 sidecar 使用 3500,以便 MyActorClient 可以依赖默认值。

现在你已成功创建了 actor 服务和客户端。请参阅相关链接部分以了解更多信息。

相关链接

4 - Dapr AI .NET SDK

快速上手 Dapr AI .NET SDK

使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。

目前,Dapr 提供对话 API 来与大语言模型进行交互。要开始使用此工作负载,请阅读 Dapr 对话 AI 操作指南。

4.1 - Dapr AI 客户端

了解如何创建 Dapr AI 客户端

Dapr AI 客户端包允许您与 Dapr 边车提供的 AI 功能进行交互。

生命周期管理

DaprConversationClient 是 Dapr 客户端的专用版本,专门用于与 Dapr Conversation API 交互。它可以与 DaprClient 和其他 Dapr 客户端一起注册,不会产生任何问题。

它维护用于与 Dapr 边车通信的 TCP 套接字形式的网络资源访问。

为获得最佳性能,请创建一个 DaprConversationClient 的单一长期实例,并在整个应用程序中提供对该共享实例的访问。DaprConversationClient 实例是线程安全的,旨在共享使用。

利用依赖注入功能可以更好地实现这一点。注册方法支持注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但同时也支持注册以使用 IConfiguration 的值或其他注入的服务,这在每个类中从头创建客户端时是不切实际的。

避免为每次操作创建一个 DaprConversationClient

通过 DaprConversationClientBuilder 配置 DaprConversationClient

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

var daprConversationClient = new DaprConversationClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
    .Build();

DaprConversationClientBuilder 包含以下设置:

  • Dapr 边车的 HTTP 端点
  • Dapr 边车的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于向边车验证请求的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 向边车发出请求时 HttpClient 实例使用的超时时间

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

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

配置 gRPC 通道选项

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

var daprConversationClient = new DaprConversationClientBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
    .Build();

使用 DaprConversationClient 进行取消操作

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

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

通过依赖注入配置 DaprConversationClient

使用内置的扩展方法在依赖注入容器中注册 DaprConversationClient 可以带来以下好处:一次性注册长期服务、集中复杂配置,并通过在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。

有三种重载可用,为开发人员在其场景中配置客户端提供了最大的灵活性。如果尚未注册 IHttpClientFactory,每种方法都会代表您注册它,并配置 DaprConversationClientBuilder 在创建 HttpClient 实例时使用它,以尽可能重用同一实例并避免套接字耗尽和其他问题。

在第一种方法中,开发人员不进行任何配置,DaprConversationClient 使用默认设置进行配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprConversationClient(); // 注册 `DaprConversationClient` 以按需注入
var app = builder.Build();

发送对话请求

使用 ConversationInputConversationOptions 向您的对话组件发送提示:

var inputs = new[]
{
    new ConversationInput(new IConversationMessage[]
    {
        new SystemMessage("You are a helpful assistant."),
        new UserMessage("Summarize the following text...")
    })
};

var options = new ConversationOptions("my-conversation-component")
{
    Temperature = 0.2
};

var response = await daprConversationClient.ConverseAsync(inputs, options);

响应格式(JSON 架构)

您可以使用 ConversationOptions.ResponseFormat 提供 JSON 架构,以将响应强制转换为特定格式。此功能需要 Dapr 运行时 v1.17.0 或更高版本。

using Google.Protobuf.WellKnownTypes;

var responseFormat = new Struct
{
    Fields =
    {
        ["type"] = new Value { StringValue = "object" },
        ["properties"] = new Value
        {
            StructValue = new Struct
            {
                Fields =
                {
                    ["answer"] = new Value
                    {
                        StructValue = new Struct
                        {
                            Fields =
                            {
                                ["type"] = new Value { StringValue = "string" }
                            }
                        }
                    }
                }
            }
        },
        ["required"] = new Value
        {
            ListValue = new ListValue { Values = { new Value { StringValue = "answer" } } }
        }
    }
};

var options = new ConversationOptions("my-conversation-component")
{
    ResponseFormat = responseFormat
};

提示缓存保留

如果您的对话组件支持提示缓存,您可以使用 ConversationOptions.PromptCacheRetention 请求缓存保留窗口。此功能需要 Dapr 运行时 v1.17.0 或更高版本。

var options = new ConversationOptions("my-conversation-component")
{
    PromptCacheRetention = TimeSpan.FromMinutes(30)
};

令牌使用统计

当组件和运行时支持时,响应包含可选的令牌使用统计。使用数据可在 ConversationResponseResult.Usage 上获得,包括总数和详细细分:

var response = await daprConversationClient.ConverseAsync(inputs, options);
var result = response.Outputs[0];

if (result.Usage is not null)
{
    var totalTokens = result.Usage.TotalTokens;
    var promptCachedTokens = result.Usage.PromptTokensDetails?.CachedTokens;
    var reasoningTokens = result.Usage.CompletionTokensDetails?.ReasoningTokens;
}

有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprConversationClientBuiler 的重载来完成的,该重载公开了配置必要选项的方法。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprConversationClient((_, daprConversationClientBuilder) => {
   // 设置 API 令牌
   daprConversationClientBuilder.UseDaprApiToken("abc123");
   // 指定非标准的 HTTP 端点
   daprConversationClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});

var app = builder.Build();

最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可能从 DaprClient 实例、供应商特定的 SDK 或某个本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载将其注入到此配置操作中:

var builder = WebApplication.CreateBuilder(args);

// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();

builder.Services.AddDaprConversationClient((serviceProvider, daprConversationClientBuilder) => {
    // 从服务提供程序检索 `SecretService` 的实例
    var secretService = serviceProvider.GetRequiredService<SecretService>();
    var daprApiToken = secretService.GetSecret("DaprApiToken").Value;

    // 配置 `DaprConversationClientBuilder`
    daprConversationClientBuilder.UseDaprApiToken(daprApiToken);
});

var app = builder.Build();

4.2 - 操作指南:在 .NET SDK 中创建和使用 Dapr AI 会话

了解如何使用 .NET SDK 创建和使用 Dapr 会话式 AI 客户端

前置条件

安装

要开始使用 Dapr AI .NET SDK 客户端,请从 NuGet 安装 Dapr.AI 包

dotnet add package Dapr.AI

DaprConversationClient 维护对用于与 Dapr 边车通信的 TCP 套接字形式的网络资源的访问。

依赖注入

AddDaprAiConversation() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入容器中,这是使用此包的推荐方式。此方法接受一个可选的选项委托来配置 DaprConversationClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是默认的 Singleton 值。

以下示例假定所有默认值都可以接受,足以注册 DaprConversationClient

services.AddDaprAiConversation();

可选的配置委托用于通过在 DaprConversationClientBuilder 上指定选项来配置 DaprConversationClient,如下例所示:

services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprAiConversation((serviceProvider, clientBuilder) => {
     //注入一个服务以从中获取值
     var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
     var standardTimeout = optionsProvider.GetStandardTimeout();
     
     //在客户端构建器上配置值
     clientBuilder.UseTimeout(standardTimeout);
});

手动实例化

除了使用依赖注入,也可以使用静态客户端构建器来构建 DaprConversationClient

为了获得最佳性能,请创建一个单一的长期存活的 DaprConversationClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprConversationClient 实例是线程安全的,旨在共享使用。

避免为每次操作创建一个 DaprConversationClient

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

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

有关通过构建器配置 Dapr 客户端时可用选项的更多信息,请参阅 .NET 文档

试用

测试 Dapr AI .NET SDK。浏览示例以查看 Dapr 的实际效果:

SDK 示例描述
SDK 示例克隆 SDK 存储库以试用一些示例并开始使用。

构建块

.NET SDK 的这一部分允许您与会话 API 交互,以从大型语言模型发送和接收消息。

4.3 - 如何:在 Dapr 的 .NET Conversation SDK 中使用 Microsoft 的 AI 扩展

了解如何创建和使用结合 Microsoft AI 扩展的 Dapr

前置条件

安装

要开始使用此 SDK,请从 NuGet 安装 Dapr.AIDapr.AI.Microsoft.Extensions 包:

dotnet add package Dapr.AI
dotnet add package Dapr.AI.Microsoft.Extensions

DaprChatClientIChatClient 接口的基于 Dapr 的实现,该接口由 Microsoft.Extensions.AI.Abstractions 包提供,使用 Dapr 的[对话构建块]({{ ref conversation-overview.md }})。它允许 开发者针对 Microsoft 提供的抽象类型进行开发,同时提供与 Dapr 对话构建块的最大一致性。由于这两种方法都采用 OpenAI 的 API 方式, 预计它们在未来会日益趋同。

关于 Microsoft.Extensions.AI

Dapr.AI.Microsoft.Extensions 包实现了 Microsoft.Extensions.AI 抽象,为 .NET 应用程序中的 AI 服务提供统一的 API。Microsoft.Extensions.AI 旨在为 不同的 AI 提供商和场景提供一致的编程模型。有关 Microsoft.Extensions.AI 的详细信息,请参阅 官方文档

服务注册

可以使用多个扩展方法将 DaprChatClient 注册到依赖注入容器中。首先, 确保注册来自 NuGet 的 Dapr.AI 包中的 DaprConversationClient

services.AddDaprConversationClient();

然后使用您的对话组件名称注册 DaprChatClient

services.AddDaprChatClient("my-conversation-component");

配置选项

您可以通过 DaprChatClientOptions 配置 DaprChatClient,尽管当前实现仅 为组件名称本身提供配置。这预计在未来的版本中会发生变化。

services.AddDaprChatClient("my-conversation-component", options => 
{
   // 在此处配置其他选项
});

您还可以配置服务生命周期(默认为 ServiceLifetime.Scoped):

services.AddDaprChatClient("my-conversation-component", ServiceLifetime.Singleton);

使用

注册后,您可以在您的服务中注入和使用 IChatClient

public class ChatService(IChatClient chatClient)
{
    public async Task<IReadOnlyList<string>> GetResponseAsync(string message)
    {
        var response = await chatClient.GetResponseAsync([
            new ChatMessage(ChatRole.User,
                "Please write me a poem in iambic pentameter about the joys of using Dapr to develop distributed applications with .NET")
            ]);
        
        return response.Messages.Select(msg => msg.Text).ToList();
    }
}

流式对话

DaprChatClient 尚不支持流式响应,使用相应的 GetStreamingResponseAsync 方法将抛出 NotImplemenetedException。一旦 Dapr 运行时 支持此功能,这预计在未来的版本中会发生变化。

工具集成

客户端通过 Microsoft.Extensions.AI 工具集成支持函数调用。注册到对话的 工具将自动可供大语言模型使用。

string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny today!" : "It's raining today!";
var toolChatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create(GetCurrentWeather, "weather")] };
var toolResponse = await chatClient.GetResponseAsync("What's the weather like today?", toolChatOptions);
foreach (var toolResp in toolResponse.Messages)
{
    Console.WriteLine(toolResp);
}

错误处理

DaprChatClient 与 Dapr 的错误处理集成,并在发生问题时抛出适当的异常。

配置和元数据

底层的 Dapr 对话组件可以通过 Dapr 对话构建块配置来配置元数据和参数。 DaprChatClient 在调用对话组件时将遵循这些设置。

最佳实践

  1. 服务生命周期:为 DaprChatClient 注册使用 ServiceLifetime.ScopedServiceLifetime.Singleton,以避免不必要地创建多个实例。

  2. 错误处理:始终将调用包装在适当的 try-catch 块中,以处理 Dapr 特定和常规异常。

  3. 资源管理DaprChatClient 通过其基类正确实现了 IDisposable,因此在使用依赖注入时会自动管理资源。

  4. 配置:正确配置您的 Dapr 对话组件以确保最佳性能和可靠性。

相关链接

5 - Dapr Jobs .NET SDK

通过 Dapr Jobs 和 Dapr .NET SDK 快速上手

使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 交互,按照预定义的计划触发未来操作运行,并支持可选的有效负载。

要开始使用,请阅读 Dapr Jobs 操作指南,并参考最佳实践文档获取更多指导。

5.1 - 操作指南:在 .NET SDK 中编写和管理 Dapr Jobs

了解如何使用 .NET SDK 编写和管理 Dapr Jobs

让我们创建一个在 Dapr Jobs 触发时会被调用的端点,然后在同一应用中调度该任务。我们将使用此处提供的简单示例进行以下演示,并以此作为说明,介绍如何使用间隔时间或 Cron 表达式来调度一次性或重复任务。在本指南中,你将:

  • 部署一个 .NET Web API 应用程序 (JobsSample)
  • 使用 Dapr .NET Jobs SDK 来调度任务调用并设置要触发的端点

在 .NET 示例项目中:

  • 主要的 Program.cs 文件包含了本演示的全部内容。

前置条件

设置环境

克隆 .NET SDK 仓库

git clone https://github.com/dapr/dotnet-sdk.git

从 .NET SDK 根目录导航到 Dapr Jobs 示例。

cd examples/Jobs

在本地运行应用程序

要运行 Dapr 应用程序,你需要启动 .NET 程序和 Dapr 边车。导航到 JobsSample 目录。

cd JobsSample

我们将运行一个同时启动 Dapr 边车和 .NET 程序的命令。

dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run

Dapr 在 http://localhost:3500 监听 HTTP 请求,在 http://localhost:4001 监听内部 Jobs gRPC 请求。

使用依赖注入注册 Dapr Jobs 客户端

Dapr Jobs SDK 提供了一个扩展方法来简化 Dapr Jobs 客户端的注册。在 Program.cs 中完成依赖注入注册之前,添加以下行:

var builder = WebApplication.CreateBuilder(args);

//Add anywhere between these two lines
builder.Services.AddDaprJobsClient();

var app = builder.Build();

请注意,在 Jobs API 的当前实现中,调度任务的应用也将是接收触发通知的应用。换句话说,你无法调度一个在另一个应用中运行的触发器。因此,虽然你不需要显式地在应用中注册 Dapr Jobs 客户端来调度触发器调用端点,但如果没有同一应用以某种方式调度任务(无论是通过此 Dapr Jobs .NET SDK 还是通过对边车的 HTTP 调用),你的端点永远不会被调用。

你可能希望为 Dapr Jobs 客户端提供一些配置选项,这些选项应在每次对边车的调用时都存在,例如 Dapr API 令牌,或者你想使用非标准的 HTTP 或 gRPC 端点。这可以通过使用注册方法的重载来实现,该重载允许配置 DaprJobsClientBuilder 实例:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) =>
{
    daprJobsClientBuilder.UseDaprApiToken("abc123");
    daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //非标准的边车 HTTP 端点
});

var app = builder.Build();

不过,你希望注入的任何值可能需要从其他来源获取,这些来源本身已注册为依赖。还有一个重载可以使用,它可以将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,该单例可以从某个地方获取密钥,并将其传递给 AddDaprJobClient 的配置方法,这样我们就可以从其他地方检索 Dapr API 令牌以在此处进行注册:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<SecretRetriever>();
builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) =>
{
    var secretRetriever = serviceProvider.GetRequiredService<SecretRetriever>();
    var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
    daprJobsClientBuilder.UseDaprApiToken(daprApiToken);

    daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512");
});

var app = builder.Build();

使用 IConfiguration 配置 Dapr Jobs 客户端

也可以使用已注册的 IConfiguration 中的值来配置 Dapr Jobs 客户端,而无需像上一节演示的那样使用 DaprJobsClientBuilder 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprJobsClient() 注册将自动使用这些值而不是各自的默认值。

首先,在配置中填充值。这可以通过几种不同的方式来完成,如下所示。

通过 ConfigurationBuilder 进行配置

可以在不使用配置源的情况下配置应用程序设置,而是通过使用 ConfigurationBuilder 实例在内存中填充值:

var builder = WebApplication.CreateBuilder();

//Create the configuration
var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string> {
            { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
            { "DAPR_API_TOKEN", "abc123" }
        })
    .Build();

builder.Configuration.AddConfiguration(configuration);
builder.Services.AddDaprJobsClient(); //这将自动从 IConfiguration 填充 HTTP 端点和 API 令牌值

通过环境变量进行配置

可以从应用程序可用的环境变量中访问应用程序设置。

以下环境变量将用于填充注册 Dapr Jobs 客户端时使用的 HTTP 端点和 API 令牌。

DAPR_HTTP_ENDPOINThttp://localhost:54321
DAPR_API_TOKENabc123
var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprJobsClient();

Dapr Jobs 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123

通过带前缀的环境变量进行配置

然而,在多个应用程序在同一台机器上运行而不使用容器的共享主机场景或开发环境中,为环境变量添加前缀并不少见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:

myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。

var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprJobsClient();

Dapr Jobs 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123

在不依赖依赖注入的情况下使用 Dapr Jobs 客户端

虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更加容易,但你并不需要以这种方式注册 DaprJobsClient。相反,你也可以选择从 DaprJobsClientBuilder 实例创建它的实例,如下所示:


public class MySampleClass
{
    public void DoSomething()
    {
        var daprJobsClientBuilder = new DaprJobsClientBuilder();
        var daprJobsClient = daprJobsClientBuilder.Build();

        //使用 `daprJobsClient` 做一些事情
    }
}

设置一个在任务触发时被调用的端点

如果你对 ASP.NET Core 中的最小 API 有点熟悉,那么设置任务端点就很简单,因为两者的语法是相同的。

完成依赖注入注册后,像处理通过 ASP.NET Core 中的最小 API 功能映射 HTTP 请求那样配置应用程序。作为扩展方法实现,传入它应该响应的任务名称和一个委托。你可以根据需要将服务注入到委托的参数中,并且可以从最初提供给任务注册的 ReadOnlyMemory<byte> 访问任务负载。

这里可以使用两个委托。如果你需要将其他服务注入到处理程序中,其中一个提供 IServiceProvider

//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient();

var app = builder.Build();

//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (IServiceProvider serviceProvider, string jobName, ReadOnlyMemory<byte> jobPayload) => {
    var logger = serviceProvider.GetService<ILogger>();
    logger?.LogInformation("Received trigger invocation for '{jobName}'", "myJob");

    //做一些事情...
});

app.Run();

如果没有必要,委托的另一个重载不需要 IServiceProvider

//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient();

var app = builder.Build();

//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
    //做一些事情...
});

app.Run();

在处理映射调用时支持取消令牌

你可能希望确保在任务调用时处理超时,这样它们就不会无限期挂起并使用系统资源。在设置任务映射时,有一个可选的 TimeSpan 参数可以作为最后一个参数提供,以指定请求的超时时间。每次触发任务映射调用时,都会使用此超时参数创建一个新的 CancellationTokenSource,并从中创建一个 CancellationToken 来限制请求的处理时间。如果未提供超时,则默认为 CancellationToken.None,并且不会自动对映射应用超时。

//我们从上面的示例中得到这个
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient();

var app = builder.Build();

//添加我们的端点注册
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
    //做一些事情...
}, TimeSpan.FromSeconds(15)); //为处理调用请求分配最大 15 秒的超时时间

app.Run();

注册任务

最后,我们必须注册要调度的任务。请注意,从这里开始,所有 SDK 方法都支持取消令牌,如果未另外设置,则使用默认令牌。

有三种不同的方式来设置任务,具体取决于你想如何配置调度。以下显示了调度任务时可用的不同参数:

参数名称类型描述必填
jobNamestring正在调度的任务的名称。
scheduleDaprJobSchedule定义任务何时触发的调度。
payloadReadOnlyMemory在触发时提供给调用端点的任务数据。
startingFromDateTime任务调度应开始的时间点。
repeatsint任务应触发的最大次数。
ttl任务何时应过期且不再触发。
overwritebool一个标志,指示提交时是否应覆盖现有任务,如果为 false,则需要先删除同名的现有任务。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

DaprJobSchedule

所有任务都是通过 SDK 使用 DaprJobSchedule 调度的,它创建一个传递给运行时的表达式来调度任务。DaprJobSchedule 上公开了几个静态方法,用于简化每种可用任务调度的注册,如下所示。这将指定任务调度本身与任何其他选项(如重复操作或提供取消令牌)分离开来。

一次性任务

一次性任务就是这样;它将在单个时间点运行,不会重复。

这种方法要求你选择一个任务名称并指定它应该被触发的时间。

DaprJobSchedule.FromDateTime(DateTimeOffset scheduledTime)

一次性任务可以从 Dapr Jobs 客户端调度,如下例所示:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken)
    {
        var today = DateTimeOffset.UtcNow;
        var threeDaysFromNow = today.AddDays(3);

        var schedule = DaprJobSchedule.FromDateTime(threeDaysFromNow);
        await daprJobsClient.ScheduleJobAsync("job", schedule, cancellationToken: cancellationToken);
    }
}

基于间隔的任务

基于间隔的任务是在配置为固定时间的循环上运行的任务,就像今天 Actors 构建块中的 提醒 的工作方式一样。

DaprJobSchedule.FromDuration(TimeSpan interval)

基于间隔的任务可以从 Dapr Jobs 客户端调度,如下例所示:

public class MyOperation(DaprJobsClient daprJobsClient)
{

    public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken)
    {
        var hourlyInterval = TimeSpan.FromHours(1);

        //每小时触发一次任务,但最多 5 次
        var schedule = DaprJobSchedule.FromDuration(hourlyInterval);
        await daprJobsClient.ScheduleJobAsync("job", schedule, repeats: 5, cancellationToken: cancellationToken);
    }
}

基于 Cron 的任务

基于 Cron 的任务是使用 Cron 表达式调度的。这提供了更多基于日历的控制,因为可以在表达式中使用基于日历的值。

DaprJobSchedule.FromCronExpression(string cronExpression)

在 Dapr SDK 中,支持两种不同的方法来调度基于 Cron 的任务。

提供你自己的 Cron 表达式

你可以通过 DaprJobSchedule.FromExpression() 通过字符串提供你自己的 Cron 表达式:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
    {
        //在每月第五天的每隔一小时的顶部
        const string cronSchedule = "0 */2 5 * *";
        var schedule = DaprJobSchedule.FromExpression(cronSchedule);

        //直到下个月才开始
        var now = DateTime.UtcNow;
        var oneMonthFromNow = now.AddMonths(1);
        var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);

        await daprJobsClient.ScheduleJobAsync("myJobName", )
        await daprJobsClient.ScheduleCronJobAsync("myJobName", schedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
    }
}

使用 CronExpressionBuilder

或者,你可以使用我们的流畅构建器来生成有效的 Cron 表达式:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
    {
        //在每月第五天的每隔一小时的顶部
        var cronExpression = new CronExpressionBuilder()
            .Every(EveryCronPeriod.Hour, 2)
            .On(OnCronPeriod.DayOfMonth, 5)
            .ToString();
        var schedule = DaprJobSchedule.FromExpression(cronExpression);

        //直到下个月才开始
        var now = DateTime.UtcNow;
        var oneMonthFromNow = now.AddMonths(1);
        var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);

        await daprJobsClient.ScheduleJobAsync("myJobName", )
        await daprJobsClient.ScheduleCronJobAsync("myJobName", schedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
    }
}

获取已调度任务的详细信息

如果你知道已调度任务的名称,你可以检索其元数据而无需等待它被触发。返回的 JobDetails 暴露了一些有用的属性,用于从 Dapr Jobs API 使用信息:

  • 如果 Schedule 属性包含 Cron 表达式,IsCronExpression 属性将为 true,表达式也可以在 CronExpression 属性中获得。
  • 如果 Schedule 属性包含持续时间值,IsIntervalExpression 属性将为 true,该值将转换为可从 Interval 属性访问的 TimeSpan 值。

可以通过使用以下内容来完成:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task<JobDetails> GetJobDetailsAsync(string jobName, CancellationToken cancellationToken)
    {
        var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken);
        return jobDetails;
    }
}

删除已调度的任务

要删除已调度的任务,你需要知道它的名称。从那里,就像在 Dapr Jobs 客户端上调用 DeleteJobAsync 方法一样简单:

public class MyOperation(DaprJobsClient daprJobsClient)
{
    public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken)
    {
        await daprJobsClient.DeleteJobAsync(jobName, cancellationToken);
    }
}

5.2 - DaprJobsClient 使用

使用 DaprJobsClient 的基本技巧和建议

生命周期管理

DaprJobsClient 是 Dapr 客户端的一个专用版本,专门用于与 Dapr Jobs API 交互。它可以与 DaprClient 及其他 Dapr 客户端一起注册,不会有任何问题。

它维护用于与 Dapr 边车通信的 TCP 套接字形式的网络资源访问,并实现 IDisposable 以支持资源的即时清理。

为了获得最佳性能,请创建一个单一的长生命周期 DaprJobsClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprJobsClient 实例是线程安全的,旨在被共享。

这可以通过利用依赖注入功能来辅助实现。注册方法支持注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但还支持注册以利用来自 IConfiguration 或其他注入服务的值,这在每个类中从头开始创建客户端时是不切实际的。

避免为每次操作创建一个 DaprJobsClient 并在操作完成时将其释放。

通过 DaprJobsClientBuilder 配置 DaprJobsClient

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

var daprJobsClient = new DaprJobsClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
    .Build();

DaprJobsClientBuilder 包含以下设置:

  • Dapr 边车的 HTTP 端点
  • Dapr 边车的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于对边车请求进行身份验证的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • SDK 在向边车发出请求时 HttpClient 实例使用的超时时间

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

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

配置 gRPC 通道选项

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

var daprJobsClient = new DaprJobsClientBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
    .Build();

在 DaprJobsClient 中使用取消操作

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

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

通过依赖注入配置 DaprJobsClient

使用内置的扩展方法在依赖注入容器中注册 DaprJobsClient 可以带来以下好处:只需一次注册长生命周期服务、集中化复杂配置,并通过在可能的情况下重新使用类似的长生命周期资源(例如 HttpClient 实例)来提高性能。

提供了三种重载,以便开发人员为其场景配置客户端时具有最大的灵活性。如果尚未注册 IHttpClientFactory,每种方法都会代表您注册它,并配置 DaprJobsClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重复使用相同的实例,并避免套接字耗尽和其他问题。

在第一种方法中,开发人员不进行任何配置,DaprJobsClient 使用默认设置进行配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient(); // 注册 `DaprJobsClient` 以便按需注入
var app = builder.Build();

有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuilder 的重载来完成的,该重载公开了用于配置必要选项的方法。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) => {
   // 设置 API 令牌
   daprJobsClientBuilder.UseDaprApiToken("abc123");
   // 指定非标准的 HTTP 端点
   daprJobsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});

var app = builder.Build();

最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可能来自 DaprClient 实例、特定于供应商的 SDK 或某些本地服务,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:

var builder = WebApplication.CreateBuilder(args);

// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();

builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => {
    // 从服务提供程序检索 `SecretService` 的实例
    var secretService = serviceProvider.GetRequiredService<SecretService>();
    var daprApiToken = secretService.GetSecret("DaprApiToken").Value;

    // 配置 `DaprJobsClientBuilder`
    daprJobsClientBuilder.UseDaprApiToken(daprApiToken);
});

var app = builder.Build();

理解 DaprJobsClient 上的负载序列化

虽然 DaprClient 上有许多方法可以使用 System.Text.Json 序列化程序自动序列化和反序列化数据,但此 SDK 采用不同的理念。相反,相关方法接受一个可选的 ReadOnlyMemory<byte> 负载,这意味着序列化是留给开发人员的练习,通常不由 SDK 处理。

也就是说,对于每种调度方法,都有一些辅助扩展方法可用。如果您知道要使用可 JSON 序列化的类型,可以对每种调度类型使用 Schedule*WithPayloadAsync 方法,该方法接受一个 object 作为负载,并接受一个可选的 JsonSerializerOptions 在序列化值时使用。为了方便起见,这将把值转换为 UTF-8 编码的字节。这是调度 Cron 表达式时的示例:

public sealed record Doodad (string Name, int Value);

//...
var doodad = new Doodad("Thing", 100);
await daprJobsClient.ScheduleCronJobWithPayloadAsync("myJob", "5 * * * *", doodad);

同样,如果您有一个纯字符串值,可以使用同一方法的重载来序列化字符串类型的负载,将跳过 JSON 序列化步骤,并且只会编码为 UTF-8 编码的字节数组。这是调度一次性作业时的示例:

var now = DateTime.UtcNow;
var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!");

处理作业调用的委托期望至少存在两个参数:

  • 一个填充了 jobNamestring,提供被调用作业的名称
  • 一个填充了作业注册期间最初提供的字节的 ReadOnlyMemory<byte>

由于负载存储为 ReadOnlyMemory<byte>,开发人员可以自由地序列化和反序列化,但同样包含两个辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。两种方法都假定开发人员对最初调度的作业进行了编码(可能使用了辅助序列化方法),因为这些方法不会强制字节表示它们不是的内容。

要将字节反序列化为字符串,可以使用以下辅助方法:

var payloadAsString = Encoding.UTF8.GetString(jobPayload.Span); // 如果成功,则返回包含值的字符串

错误处理

如果在 SDK 和运行在 Dapr 边车上的 Jobs API 服务之间遇到问题,DaprJobsClient 上的方法将抛出 DaprJobsServiceException。如果由于通过此 SDK 向 Jobs API 服务发出的格式错误的请求而导致失败,将抛出 DaprMalformedJobException。在非法参数值的情况下,将抛出相应的标准异常(例如 ArgumentOutOfRangeExceptionArgumentNullException)以及违规参数的名称。对于其他任何情况,将抛出 DaprException

最常见的失败情况与以下内容相关:

  • 与 Jobs API 交互时参数格式不正确
  • 瞬态故障,例如网络问题
  • 无效数据,例如无法将值反序列化回最初未序列化的类型

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

6 - Dapr Cryptography .NET SDK

快速上手 Dapr Cryptography .NET SDK

使用 Dapr Cryptography 包,您可以执行高性能的加密和解密操作。

要开始使用此功能,请阅读 [Dapr Cryptography(https://docs.dapr.io/zh-hans/developing-applications/sdks/dotnet/dotnet-cryptography/dotnet-cryptography-howto/) 操作指南。

6.1 - Dapr Cryptography Client

了解如何创建 Dapr Cryptography 客户端

Dapr Cryptography 包允许您执行由 Dapr 边车提供的加密和解密操作。

生命周期管理

DaprEncryptionClient 是 Dapr 客户端的专用版本,专门用于与 Dapr Cryptography API 交互。它可以与 DaprClient 及其他 Dapr 客户端一起注册而不会出现问题。

它维护与网络资源的连接,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。

为了获得最佳性能,请创建一个 DaprEncryptionClient 的长期实例,并在整个应用程序中提供对该共享实例的访问。DaprEncryptionClient 实例是线程安全的,旨在共享使用。

利用依赖注入功能可以辅助实现这一点。注册方法支持将其注册为单例、作用域实例或瞬态(意味着每次注入时都会重新创建),此外还支持注册时使用来自 IConfiguration 或其他注入服务的值,这在每个类中从头创建客户端时是不切实际的。

避免为每次操作都创建一个 DaprEncryptionClient

通过 DaprEncryptionClientBuilder 配置 DaprEncryptionClient

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

var daprEncryptionClient = new DaprEncryptionClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于向 Dapr 边车进行身份验证的 API 令牌
    .Build();

DaprEncryptionClientBuilder 包含以下设置:

  • Dapr 边车的 HTTP 端点
  • Dapr 边车的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于向边车验证请求的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 在向边车发出请求时 HttpClient 实例使用的超时时间

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

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

配置 gRPC 通道选项

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

var daprEncryptionClient = new DaprEncryptionClientBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { .. ThrowOperationCanceledOnCancellation = true })
    .Build();

使用 DaprEncryptionClient 进行取消操作

DaprEncryptionClient 上的 API 执行异步操作并接受可选的 CancellationToken 参数。这遵循了 .NET 中可取消操作的标准实践。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。

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

通过依赖注入配置 DaprEncryptionClient

使用内置扩展方法在依赖注入容器中注册 DaprEncryptionClient 可以带来以下好处:只需注册一次长期服务、集中复杂配置,并通过在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。

提供了三种重载,以便开发人员为其场景配置客户端时获得最大的灵活性。如果尚未注册 IHttpClientFactory,每个重载都将代表您注册它,并配置 DaprEncryptionClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。

在第一种方法中,开发人员不进行任何配置,DaprEncryptionClient 使用默认设置进行配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprEncryptionClent(); // 注册 `DaprEncryptionClient` 以便在需要时注入
var app = builder.Build();

有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprEncryptionClientBuiler 的重载来完成的,并公开了配置必要选项的方法。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprEncryptionClient((_, daprEncrpyptionClientBuilder) => {
   // 设置 API 令牌
   daprEncryptionClientBuilder.UseDaprApiToken("abc123");
   // 指定非标准 HTTP 端点
   daprEncryptionClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});

var app = builder.Build();

最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某些本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:

var builder = WebApplication.CreateBuilder(args);

// 注册从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();

builder.Services.AddDaprEncryptionClient((serviceProvider, daprEncryptionClientBuilder) => {
    // 从服务提供程序检索 `SecretService` 的实例
    var secretService = serviceProvider.GetRequiredService<SecretService>();
    var daprApiToken = secretService.GetSecret("DaprApiToken").Value;

    // 配置 `DaprEncryptionClientBuilder`
    daprEncryptionClientBuilder.UseDaprApiToken(daprApiToken);
});

var app = builder.Build();

6.2 - 如何操作:在 .NET SDK 中创建和使用 Dapr Cryptography

了解如何使用 .NET SDK 创建和使用 Dapr Cryptography 客户端

前置条件

安装

要开始使用 Dapr Cryptography 客户端,请从 NuGet 安装 Dapr.Cryptography 包

dotnet add package Dapr.Cryptography

DaprEncryptionClient 保持对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在。

依赖注入

AddDaprEncryptionClient() 方法会将 Dapr 客户端注册到依赖注入容器中,这是使用此包的推荐方式。该方法接受一个可选的 options 委托用于配置 DaprEncryptionClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是使用默认的 Singleton 值。

以下示例假设所有默认值都是可接受的,足以注册 DaprEncryptionClient

services.AddDaprEncryptionClient();

可选的配置委托用于通过在 DaprEncryptionClientBuilder 上指定选项来配置 DaprEncryptionClient,如下例所示:

services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprEncryptionClient((serviceProvider, clientBuilder) => {
     // 注入一个服务以从中获取值
     var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
     var standardTimeout = optionsProvider.GetStandardTimeout();
     
     // 在客户端构建器上配置值
     clientBuilder.UseTimeout(standardTimeout);
});

手动实例化

除了使用依赖注入外,也可以使用静态客户端构建器构建 DaprEncryptionClient

为了获得最佳性能,请创建单个长期存在的 DaprEncryptionClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprEncryptionClient 实例是线程安全的,旨在共享。

避免为每次操作创建 DaprEncryptionClient

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

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

有关通过构建器配置 Dapr 客户端时可用选项的更多信息,请参阅 .NET 文档

试用一下

测试 Dapr AI .NET SDK。浏览示例以查看 Dapr 的实际应用:

SDK 示例描述
SDK 示例克隆 SDK 存储库以尝试一些示例并开始使用。

7 - Dapr Messaging .NET SDK

快速上手 Dapr Messaging .NET SDK

使用 Dapr Messaging 包,你可以从 .NET 应用程序与 Dapr messaging API 交互。在 v1.15 版本中,此包仅包含与流式发布订阅功能对应的功能。

未来的 Dapr .NET SDK 版本会将现有的 messaging 功能从 Dapr.Client 迁移到此 Dapr.Messaging 包。这将提前在发行说明、文档和过时属性中进行说明。

要开始使用,请阅读 Dapr Messaging 操作指南,并参考最佳实践文档获取更多指导。

7.1 - 如何操作:使用 .NET SDK 创建和管理 Dapr 流式订阅

了解如何使用 .NET SDK 创建和管理 Dapr 流式订阅

让我们使用流式处理能力创建一个对发布/订阅主题或队列的订阅。在接下来的演示中,我们将使用此处提供的简单示例,作为说明如何配置消息处理程序的指南,这些配置在运行时进行,无需预先配置端点。在本指南中,您将:

前置条件

设置环境

克隆 .NET SDK 仓库

git clone https://github.com/dapr/dotnet-sdk.git

从 .NET SDK 根目录导航到 Dapr 流式 PubSub 示例。

cd examples/Client/PublishSubscribe

在本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和 Dapr 边车。导航到 StreamingSubscriptionExample 目录。

cd StreamingSubscriptionExample

我们将运行一个同时启动 Dapr 边车和 .NET 程序的命令。

dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run

Dapr 在 http://localhost:3500 监听 HTTP 请求,在 http://localhost:4001 监听内部 Jobs gRPC 请求。

使用依赖注入注册 Dapr PubSub 客户端

Dapr Messaging SDK 提供了一个扩展方法来简化 Dapr PubSub 客户端的注册。在完成 Program.cs 中的依赖注入注册之前,添加以下行:

var builder = WebApplication.CreateBuilder(args);

//Add anywhere between these two
builder.Services.AddDaprPubSubClient(); //That's it

var app = builder.Build();

您可能需要为 Dapr PubSub 客户端提供一些配置选项,这些选项应在每次对边车的调用时都存在,例如 Dapr API 令牌,或者您想使用非标准的 HTTP 或 gRPC 端点。这可以通过使用注册方法的重载来实现,该方法允许配置 DaprPublishSubscribeClientBuilder 实例:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprPubSubClient((_, daprPubSubClientBuilder) => {
    daprPubSubClientBuilder.UseDaprApiToken("abc123");
    daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint
});

var app = builder.Build();

不过,您可能希望从其他来源检索要注入的值,而这些值本身已注册为依赖项。您还可以使用另一个重载将 IServiceProvider 注入到配置操作方法中。在以下示例中,我们注册一个虚拟的单例,该单例可以从某处检索机密,并将其传递给 AddDaprJobClient 的配置方法,以便我们可以从其他地方检索我们的 Dapr API 令牌以在此处进行注册:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<SecretRetriever>();
builder.Services.AddDaprPubSubClient((serviceProvider, daprPubSubClientBuilder) => {
    var secretRetriever = serviceProvider.GetRequiredService<SecretRetriever>();
    var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
    daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
    
    daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512");
});

var app = builder.Build();

使用 IConfiguration 配置 Dapr PubSub 客户端

也可以使用已注册的 IConfiguration 中的值来配置 Dapr PubSub 客户端,而无需按照上一节演示的那样使用 DaprPublishSubscribeClientBuilder 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprPubSubClient() 注册将自动使用这些值而不是其各自的默认值。

首先填充配置中的值。可以通过以下几种不同方式来完成此操作,如下所示。

通过 ConfigurationBuilder 配置

可以在不使用配置源的情况下配置应用程序设置,而是使用 ConfigurationBuilder 实例在内存中填充值:

var builder = WebApplication.CreateBuilder();

//Create the configuration
var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string> {
            { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
            { "DAPR_API_TOKEN", "abc123" }
        })
    .Build();

builder.Configuration.AddConfiguration(configuration);
builder.Services.AddDaprPubSubClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration

通过环境变量配置

可以从应用程序可用的环境变量访问应用程序设置。

以下环境变量将用于填充注册 Dapr PubSub 客户端所使用的 HTTP 端点和 API 令牌。

DAPR_HTTP_ENDPOINThttp://localhost:54321
DAPR_API_TOKENabc123
var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprPubSubClient();

Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123

通过带前缀的环境变量配置

但是,在不使用容器的共享主机场景中(在同一台机器上运行多个应用程序)或在开发环境中,为环境变量添加前缀并不罕见。以下示例假定 HTTP 端点和 API 令牌都将从带有值 “myapp_” 前缀的环境变量中提取。在此场景中使用的两个环境变量如下:

myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。

var builder = WebApplication.CreateBuilder();

builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprPubSubClient();

Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123

不依赖依赖注入使用 Dapr PubSub 客户端

虽然使用依赖注入简化了 .NET 中复杂类型的使用,并使处理复杂配置变得更加容易,但您不需要以这种方式注册 DaprPublishSubscribeClient。相反,您也可以选择从 DaprPublishSubscribeClientBuilder 实例创建它的实例,如下所示:


public class MySampleClass
{
    public void DoSomething()
    {
        var daprPubSubClientBuilder = new DaprPublishSubscribeClientBuilder();
        var daprPubSubClient = daprPubSubClientBuilder.Build();

        //Do something with the `daprPubSubClient`
    }
}

设置消息处理程序

Dapr 中的流式订阅实现通过将消息保留在 Dapr 运行时中,直到您的应用程序准备好接受它们为止,从而为您提供更大的控制权来处理来自事件的背压。.NET SDK 支持高性能队列,用于在处理挂起时在应用程序中维护这些消息的本地缓存。这些消息将保留在队列中,直到每个消息的处理超时或对每个消息执行响应操作(通常在处理成功或失败之后)。在 Dapr 运行时收到此响应操作之前,消息将由 Dapr 持久化,并在服务故障的情况下可用。

可用的各种响应操作如下:

响应操作描述
Retry该事件应在将来再次传递。
Drop该事件应被删除(或转发到死信队列,如果已配置)并且不再尝试。
Success该事件应被删除,因为它已成功处理。

处理程序一次将只接收一条消息,如果向订阅提供了取消令牌,则该令牌将在处理程序调用期间提供。

处理程序必须配置为返回 Task<TopicResponseAction>,以指示这些操作之一,即使来自 try/catch 块。如果您的处理程序未捕获异常,订阅将使用订阅注册期间在选项中配置的响应操作。

以下演示了示例中提供的示例消息处理程序:

Task<TopicResponseAction> HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
{
    try
    {
        //Do something with the message
        Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
        return Task.FromResult(TopicResponseAction.Success);
    }
    catch
    {
        return Task.FromResult(TopicResponseAction.Retry);
    }
}

配置和订阅 PubSub 主题

流式订阅的配置需要向 Dapr 注册的 PubSub 组件的名称、正在订阅的主题或队列的名称、提供订阅配置的 DaprSubscriptionOptions、消息处理程序和可选的取消令牌。DaprSubscriptionOptions 唯一必需的参数是默认的 MessageHandlingPolicy,它由每个事件的超时和在该超时时采取的 TopicResponseAction 组成。

其他选项如下:

属性名称描述
元数据附加订阅元数据
死信主题用于将丢弃的消息发送到的死信主题的可选名称。
最大排队消息数默认情况下,不会对内部队列强制执行最大边界,但设置此
属性将强加一个上限。
最大清理超时当订阅被释放或令牌发出取消请求时,这指定
可用于处理内部队列中剩余消息的最长时间。

然后,订阅将配置为以下示例:

var messagingClient = app.Services.GetRequiredService<DaprPublishSubscribeClient>();

var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); //Override the default of 30 seconds
var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry));
var subscription = await messagingClient.SubscribeAsync("pubsub", "mytopic", options, HandleMessageAsync, cancellationTokenSource.Token);

终止和清理订阅

当您完成订阅并希望停止接收新事件时,只需在订阅实例上等待对 DisposeAsync() 的调用。这将导致客户端取消注册其他事件,并在释放任何内部资源之前继续处理背压队列中仍然剩余的所有事件(如果有)。此清理将受到订阅注册时在 DaprSubscriptionOptions 中提供的超时间隔的限制,默认情况下,这设置为 30 秒。

7.2 - DaprPublishSubscribeClient 用法

使用 DaprPublishSubscribeClient 的基本技巧与建议

生命周期管理

DaprPublishSubscribeClient 是 Dapr 客户端的一个版本,专门用于与 Dapr 消息传递 API 交互。 它可以与 DaprClient 和其他 Dapr 客户端一起注册而不会产生问题。

它维护对网络资源的访问,这些资源以用于与 Dapr 边车通信的 TCP 套接字形式存在,并实现 IAsyncDisposable 以支持资源的积极清理。

为获得最佳性能,应创建一个 DaprPublishSubscribeClient 的单一长期实例,并在整个应用程序中提供对该共享 实例的访问。DaprPublishSubscribeClient 实例是线程安全的,旨在共享使用。

可以利用依赖注入功能来辅助实现这一点。注册方法支持注册为 单例、作用域实例或瞬态(意味着每次注入时都会重新创建),但同时也支持 利用 IConfiguration 或其他注入服务的值进行注册,这在每次 从头创建客户端时是不切实际的。

避免为每个操作创建一个 DaprPublishSubscribeClient 并在操作完成时将其释放。 DaprPublishSubscribeClient 仅应在您不再希望在订阅上接收事件时才被释放,因为释放它将取消新事件的持续接收。

通过 DaprPublishSubscribeClientBuilder 配置 DaprPublishSubscribeClient

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

var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
    .UseDaprApiToken("abc123") // 指定用于向其他 Dapr 边车进行身份验证的 API 令牌
    .Build();

DaprPublishSubscribeClientBuilder 包含以下设置:

  • Dapr 边车的 HTTP 端点
  • Dapr 边车的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于向边车验证请求的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 在向边车发出请求时 HttpClient 实例使用的超时时间

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

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

配置 gRPC 通道选项

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

var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
    .Build();

在 DaprPublishSubscribeClient 中使用取消

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

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

通过依赖注入配置 DaprPublishSubscribeClient

使用用于在依赖注入容器中注册 DaprPublishSubscribeClient 的内置扩展方法 可以带来以下好处:一次性注册长期服务、集中复杂配置,并通过确保类似的长期资源在可能的情况下被重用(例如 HttpClient 实例)来提高性能。

有三种重载可用,为开发人员在其场景中配置客户端提供最大的灵活性。 如果尚未注册,每个重载都将代表您注册 IHttpClientFactory,并配置 DaprPublishSubscribeClientBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。

在第一种方法中,开发人员不进行任何配置,DaprPublishSubscribeClient 使用默认设置进行配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.DaprPublishSubscribeClient(); //注册 `DaprPublishSubscribeClient` 以便根据需要注入
var app = builder.Build();

有时开发人员需要使用上述各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler 的重载来完成的,该重载公开了配置必要选项的方法。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprJobsClient((_, daprPubSubClientBuilder) => {
   //设置 API 令牌
   daprPubSubClientBuilder.UseDaprApiToken("abc123");
   //指定非标准的 HTTP 端点
   daprPubSubClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});

var app = builder.Build();

最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某个本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:

var builder = WebApplication.CreateBuilder(args);

//注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();

builder.Services.AddDaprPublishSubscribeClient((serviceProvider, daprPubSubClientBuilder) => {
    //从服务提供程序检索 `SecretService` 的实例
    var secretService = serviceProvider.GetRequiredService<SecretService>();
    var daprApiToken = secretService.GetSecret("DaprApiToken").Value;

    //配置 `DaprPublishSubscribeClientBuilder`
    daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
});

var app = builder.Build();

8 - Dapr 分布式锁 .NET SDK

快速上手 Dapr 分布式锁 .NET SDK

使用 Dapr 分布式锁包,您可以在资源上创建和移除锁,以管理分布式应用程序之间的独占性。

虽然此功能在 Dapr.ClientDapr.DistributedLock 包中均有实现,但两者之间的方法略有不同,未来版本将弃用 Dapr.Client 包。建议新实现使用 Dapr.DistributedLock 包。本文档将反映 Dapr.DistributedLock 包中的实现。

生命周期管理

DaprDistributedLockClient 是专门用于与 Dapr 分布式锁 API 交互的 Dapr 客户端版本。它可以与 DaprClient 和其他 Dapr 客户端一起注册,不会有任何问题。

它维护对网络资源的访问,这些资源以用于与 Dapr 边车运行时通信的 TCP 套接字形式存在。

为了获得最佳性能,建议您利用 Dapr.DistributedLock 包提供的依赖注入容器机制,以便在整个应用程序中轻松访问注入的实例。这些注入的实例是线程安全的,旨在应用程序的不同类型之间使用。通过依赖注入注册可以利用 IConfiguration 中的值或其他注入的服务,这在每个类中从头创建客户端时是不切实际的。

如果您选择手动创建 DaprDistributedLockClient 实例,建议使用 DaprClientBuilder 来创建客户端。这将确保客户端正确配置以与 Dapr 边车运行时通信。

避免为每个操作创建 DaprDistributedLockClient

通过 DaprDistributedLockBuilder 配置 DaprDistributedLockClient

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

var daprDistributedLockClient = new DaprDistributedLockBuilder()
    .UseDaprApiToken("abc123") // 可选地指定用于向其他 Dapr 边车进行身份验证的 API 令牌
    .Build();

DaprDistributedLockBuilder 包含以下设置:

  • Dapr 边车的 HTTP 端点
  • Dapr 边车的 gRPC 端点
  • 用于配置 JSON 序列化的 JsonSerializerOptions 对象
  • 用于配置 gRPC 的 GrpcChannelOptions 对象
  • 用于向边车验证请求的 API 令牌
  • 用于创建 SDK 使用的 HttpClient 实例的工厂方法
  • 向边车发出请求时 HttpClient 实例使用的超时时间

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

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

配置 gRPC 通道选项

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

var daprDistributedLockClient = new DaprDistributedLockBuilder()
    .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
    .Build();

DaprDistributedLockClient 中使用取消

DaprDistributedLockClient 上的 API 执行异步操作并接受可选的 CancellationToken 参数。这遵循了 .NET 可取消操作的标准实践。请注意,当发生取消时,无法保证远程端点停止处理请求,只能保证客户端已停止等待完成。

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

通过依赖注入配置 DaprDistributedLockClient

使用内置扩展方法在依赖注入容器中注册 DaprDistributedLockClient 可以提供一次注册长期服务的优势,集中化复杂配置,并通过确保在可能的情况下重用类似的长期资源(例如 HttpClient 实例)来提高性能。

有三种重载可用,为开发人员为其场景配置客户端提供最大的灵活性。如果尚未注册,每个重载都会代表您注册 IHttpClientFactory,并配置 DaprDistributedLockBuilder 在创建 HttpClient 实例时使用它,以便尽可能重用同一实例并避免套接字耗尽和其他问题。

在第一种方法中,开发人员不进行任何配置,DaprDistributedLockClient 使用默认设置配置。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprDistributedLock(); // 根据需要注册 `DaprDistributedLockClient` 以进行注入
var app = builder.Build();

有时开发人员需要使用上面详述的各种配置选项来配置创建的客户端。这是通过传入 DaprDistributedLockBuilder 并公开配置必要选项的方法的重载来完成的。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprDistributedLock((_, daprDistributedLockBuilder) => {
   // 设置 API 令牌
   daprDistributedLockBuilder.UseDaprApiToken("abc123");
   // 指定非标准的 HTTP 端点
   daprDistributedLockBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});

var app = builder.Build();

最后,开发人员可能需要从另一个服务检索信息以填充这些配置值。该值可以从 DaprClient 实例、供应商特定的 SDK 或某些本地服务提供,但只要它也在 DI 中注册,就可以通过最后一个重载注入到此配置操作中:

var builder = WebApplication.CreateBuilder(args);

// 注册一个从某处检索机密的虚构服务
builder.Services.AddSingleton<SecretService>();

builder.Services.AddDaprDistributedLock((serviceProvider, daprDistributedLockBuilder) => {
    // 从服务提供程序检索 `SecretService` 的实例
    var secretService = serviceProvider.GetRequiredService<SecretService>();
    var daprApiToken = secretService.GetSecret("DaprApiToken").Value;

    // 配置 `DaprDistributedLockBuilder`
    daprDistributedLockBuilder.UseDaprApiToken(daprApiToken);
});

var app = builder.Build();

8.1 - 操作指南:在 .NET SDK 中创建和使用 Dapr 分布式锁

了解如何使用 .NET SDK 创建和使用 Dapr 分布式锁客户端

前置条件

安装

要开始使用 Dapr 分布式锁 .NET SDK 客户端,请从 NuGet 安装 Dapr.Distributed Lock 包

dotnet add package Dapr.DistributedLock

DaprDistributedLockClient 以 TCP 套接字的形式维护对网络资源的访问,用于与 Dapr 边车通信。

依赖注入

AddDaprDistributedLock() 方法会将 Dapr 客户端注册到 ASP.NET Core 依赖注入中,这是使用此包的推荐方式。此方法接受一个可选的 options 委托用于配置 DaprDistributedLockClient,以及一个 ServiceLifetime 参数,允许您为注册的服务指定不同的生命周期,而不是使用默认的 Singleton 值。

以下示例假定所有默认值均可接受,足以注册 DaprDistributedLockClient

services.AddDaprDistributedLock();

可选的 configuration 委托用于通过在 DaprDistributedLockBuilder 上指定选项来配置 DaprDistributedLockClient,如以下示例所示:

services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprDistributedLock((serviceProvider, clientBuilder) => {
     // 注入服务以从中获取值
     var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
     var standardTimeout = optionsProvider.GetStandardTimeout();
     
     // 在客户端构建器上配置值
     clientBuilder.UseTimeout(standardTimeout);
});

手动实例化

除了使用依赖注入,也可以使用静态客户端构建器来构建 DaprDistributedLockClient

为了获得最佳性能,请创建一个单一的长生命周期 DaprDistributedLockClient 实例,并在整个应用程序中提供对该共享实例的访问。DaprDistributedLockClient 实例是线程安全的,旨在共享使用。

避免为每次操作创建一个 DaprDistributedLockClient

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

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

有关通过构建器配置 Dapr 分布式锁客户端时可用的选项的更多信息,请参阅 .NET 文档此处

试用

试用 Dapr 分布式锁 .NET SDK。浏览示例,查看 Dapr 的实际运行:

SDK 示例描述
SDK 示例克隆 SDK 存储库以尝试一些示例并开始使用。

构建块

.NET SDK 的这一部分允许您与分布式锁 API 交互,以放置和移除锁,从而管理分布式应用程序中的资源独占性。

9 - Dapr .NET SDK 最佳实践

高效使用 Dapr .NET SDK

以信心构建

Dapr .NET SDK 提供了丰富的功能集用于构建分布式应用。本节提供在生产场景中高效使用 SDK 的实用指南——聚焦于可靠性、可维护性和开发者体验。

涵盖的主题包括:

  • Dapr 构建块中的错误处理策略
  • 管理实验性功能并抑制相关警告
  • 利用源代码分析器和生成器来减少样板代码并及早发现问题
  • 使用 Dapr.Testcontainers 运行集成测试
  • 基于 Dapr 应用的通用 .NET 开发实践

错误模型指南

Dapr 操作可能因多种原因而失败——网络问题、组件配置错误或瞬态故障。SDK 提供了结构化的错误类型,帮助您区分可重试错误和致命错误。

了解如何有效使用 DaprException 及其派生类型详见此处

实验性属性

部分 SDK 功能被标记为实验性,可能在未来的版本中发生变化。这些功能使用 [Experimental] 进行标注,默认情况下会生成构建时警告。您可以:

  • 使用 #pragma warning disable 选择性抑制警告
  • 使用 SuppressMessage 属性进行更精细的控制
  • 跟踪整个代码库中的实验性功能使用情况

了解更多关于 [Experimental] 属性的使用详见此处

源代码工具

SDK 包含基于 Roslyn 的分析器和源代码生成器,帮助您更轻松地编写更好的代码。这些工具能够:

  • 对 SDK 的常见误用发出警告
  • 为 Actor 注册和调用生成样板代码
  • 支持 IDE 集成以提供更快的反馈

阅读更多关于如何安装和使用这些分析器的内容详见此处

其他指南

本节旨在支持广泛的开发场景。随着应用复杂性的增长,您会发现越来越多与 .NET 中 Dapr 相关的实践和模式——从 Actor 生命周期管理到配置策略和性能调优。

了解如何使用 Dapr.Testcontainers 运行集成测试详见此处

9.1 - Dapr .NET SDK 中的错误模型

了解如何使用 .NET SDK 中更丰富的错误模型。

Dapr .NET SDK 支持 Dapr 运行时实现的更丰富的错误模型。该模型为应用程序提供了一种使用额外上下文来丰富错误的方式, 使应用程序的使用者能够更好地理解问题并更快地解决它。您可以在这里阅读有关更丰富的错误模型的更多信息,并可以在这里找到实现这些错误的 Dapr proto 文件。

Dapr .NET SDK 实现了 Dapr 运行时支持的所有详细信息,在 Dapr.Common.Exceptions 命名空间中实现,并通过 DaprException 扩展方法 TryGetExtendedErrorInfo 访问。目前,此详细信息提取仅支持存在详细信息的 RpcException

// ExtendedErrorInfo 的使用示例

try
{
    // 使用 Dapr 客户端执行某些会抛出 DaprException 的操作。
}
catch (DaprException daprEx)
{
    if (daprEx.TryGetExtendedErrorInfo(out DaprExtendedErrorInfo errorInfo))
    {
        Console.WriteLine(errorInfo.Code);
        Console.WriteLine(errorInfo.Message);

        foreach (DaprExtendedErrorDetail detail in errorInfo.Details)
        {
            Console.WriteLine(detail.ErrorType);
            switch (detail.ErrorType)
            {
                case ExtendedErrorType.ErrorInfo:
                    Console.WriteLine(detail.Reason);
                    Console.WriteLine(detail.Domain);
                    break;
                default:
                    Console.WriteLine(detail.TypeUrl);
                    break;
            }
        }
    }
}

DaprExtendedErrorInfo

包含与错误关联的 Code(状态代码)和 Message(错误消息),从内部的 RpcException 解析而来。 还包含从异常的详细信息中解析的 DaprExtendedErrorDetails 集合。

DaprExtendedErrorDetail

所有详细信息都实现抽象的 DaprExtendedErrorDetail 并具有关联的 DaprExtendedErrorType

  1. RetryInfo

  2. DebugInfo

  3. QuotaFailure

  4. PreconditionFailure

  5. RequestInfo

  6. LocalizedMessage

  7. BadRequest

  8. ErrorInfo

  9. Help

  10. ResourceInfo

  11. Unknown

RetryInfo

通知客户端在重试之前应等待多长时间的信息。提供一个带有 Second(秒偏移量)和 Nano(纳秒偏移量)属性的 DaprRetryDelay

DebugInfo

服务器提供的调试信息。包含 StackEntries(包含堆栈跟踪的字符串集合)和 Detail(进一步的调试信息)。

QuotaFailure

与可能已达到的配额相关的信息,例如 API 的每日使用限制。它有一个属性 ViolationsDaprQuotaFailureViolation 的集合,每个集合包含一个 Subject(请求的主题)和 Description(有关失败的更多信息)。

PreconditionFailure

通知客户端某些必需的前提条件未满足的信息。有一个属性 ViolationsDaprPreconditionFailureViolation 的集合, 每个集合具有 Subject(前提条件失败发生的主题,例如 “Azure”)、Type(前提条件类型的表示,例如 “TermsOfService”)和 Description(进一步描述,例如 “ToS must be accepted.")。

RequestInfo

服务器返回的信息,服务器可以使用该信息来标识客户端的请求。包含 RequestIdServingData 属性, RequestId 是服务器可以解释的某个字符串(例如 UID),ServingData 是构成请求的一部分的任意数据。

LocalizedMessage

包含本地化消息以及消息的区域设置。包含 Locale(区域设置,例如 “en-US”)和 Message(本地化消息)。

BadRequest

描述错误的请求字段。包含 DaprBadRequestDetailFieldViolation 集合,每个集合具有 Field(请求中的错误字段,例如 ‘first_name’)和 Description(详细说明原因的更多信息,例如 “first_name cannot contain special characters”)。

ErrorInfo

详细说明错误的原因。包含三个属性:Reason(错误的原因,应采用 UPPER_SNAKE_CASE 的形式,例如 DAPR_INVALID_KEY)、 Domain(错误所属的域,例如 ‘dapr.io’)和 Metadata,一个包含更多信息的基于键/值的集合。

Help

为客户端提供资源以对问题进行进一步研究。包含 DaprHelpDetailLink 集合, 该集合提供 Url(指向帮助或文档的 url)和 Description(对链接提供的描述)。

ResourceInfo

提供与访问的资源相关的信息。提供三个属性:ResourceType(正在访问的资源的类型,例如 “Azure service bus”)、 ResourceName(资源的名称,例如 “my-configured-service-bus”)、Owner(资源的所有者,例如 “subscriptionowner@dapr.io”)和 Description(与错误相关的资源的更多信息,例如 “missing permissions to use this resource”)。

Unknown

当详细信息类型 url 无法映射到正确的 DaprExtendedErrorDetail 实现时返回。 提供一个属性 TypeUrl(无法解析的类型 url,例如 “type.googleapis.com/Google.rpc.UnrecognizedType”)。

9.2 - Experimental Attributes

了解为什么我们用 [Experimental] 属性标记某些方法

Experimental Attributes

Experimental Attributes 简介

随着 .NET 8 的发布,C# 12 引入了 [Experimental] 属性,它提供了一种标准化的方式来标记仍在开发或实验阶段的 API。该属性定义在 System.Diagnostics.CodeAnalysis 命名空间中,需要一个诊断 ID 参数,用于在使用实验性 API 时生成编译器警告。

在 Dapr .NET SDK 中,我们现在使用 [Experimental] 属性而不是 [Obsolete] 来标记尚未通过稳定生命周期认证的构建块和组件。这种方法提供了更清晰的区分:

  1. Experimental APIs — 已经可用但仍在演进的功能,尚未根据 Dapr Component Certification Lifecycle 认证为稳定。

  2. Obsolete APIs — 真正已被弃用并将在未来版本中移除的功能。

在 Dapr .NET SDK 中的使用

在 Dapr .NET SDK 中,我们在类级别为仍处于 Component Certification Lifecycle 的 Alpha 或 Beta 阶段的构建块应用 [Experimental] 属性。该属性包括:

  • 一个用于标识实验性构建块的诊断 ID
  • 一个指向该构建块相关文档的 URL

例如:

using System.Diagnostics.CodeAnalysis;
namespace Dapr.Cryptography.Encryption 
{ 
    [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] 
    public class DaprEncryptionClient 
    { 
        // Implementation 
    } 
}

诊断 ID 遵循 DAPR_[BUILDING_BLOCK_NAME] 的命名约定,例如:

  • DAPR_CONVERSATION — 用于 Conversation 构建块
  • DAPR_CRYPTOGRAPHY — 用于 Cryptography 构建块
  • DAPR_JOBS — 用于 Jobs 构建块
  • DAPR_DISTRIBUTEDLOCK — 用于 Distributed Lock 构建块

抑制 Experimental 警告

当您使用标记有 [Experimental] 属性的 API 时,编译器会生成错误。要在不将自己的代码标记为实验性的情况下构建解决方案,您需要抑制这些错误。以下是几种方法:

选项 1:使用 #pragma 指令

您可以使用 #pragma warning 指令来抑制特定代码段的警告:

// Disable experimental warning 
#pragma warning disable DAPR_CRYPTOGRAPHY 
// Your code using the experimental API 
var client = new DaprEncryptionClient(); 
// Re-enable the warning 
#pragma warning restore DAPR_CRYPTOGRAPHY

这种方法适用于只想抑制代码中特定部分的警告。

选项 2:项目级抑制

要为整个项目抑制警告,请将以下内容添加到您的 .csproj 文件中。

<PropertyGroup>
    <NoWarn>$(NoWarn);DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>

您可以包含多个用分号分隔的诊断 ID:

<PropertyGroup>
    <NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>

这种方法对于需要使用实验性 API 的测试项目特别有用。

选项 3:目录级抑制

要为目录中的多个项目抑制警告,请添加一个 Directory.Build.props 文件:

<PropertyGroup>
    <NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
</PropertyGroup>

该文件应放置在测试项目的根目录中。您可以在 MSBuild 文档 中了解更多关于使用 Directory.Build.props 文件的信息。

Experimental API 的生命周期

随着构建块通过认证生命周期并达到 “Stable” 阶段,[Experimental] 属性将被移除。此时用户无需进行任何迁移或代码更改,只需移除之前添加的警告抑制即可。

相反,[Obsolete] 属性现在将专门保留给真正被弃用并计划移除的 API。当您看到标记有 [Obsolete] 的方法或类时,应根据属性消息中提供的迁移指导计划迁移。

最佳实践

  1. 在应用程序代码中:

    • 谨慎使用实验性 API,因为它们可能会在未来版本中发生变化
    • 考虑将实验性 API 的使用隔离,以便将来更容易更新
    • 记录您对实验性 API 的使用,以便团队了解
  2. 在测试代码中:

    • 使用项目级抑制以避免在测试代码中充斥警告抑制
    • 定期审查您使用的实验性 API,并检查它们是否已稳定
  3. 在为 SDK 做贡献时:

    • 对于尚未完成认证的新构建块使用 [Experimental]
    • 仅对真正被弃用的 API 使用 [Obsolete]
    • UrlFormat 参数中提供清晰的文档链接

其他资源

9.3 - Integration testing with Dapr.Testcontainers

使用 Dapr.Testcontainers 针对真实基础设施运行 Dapr 集成测试

概述

Dapr.Testcontainers 是一个辅助包,用于使用容器针对真实的 Dapr 运行时组件编写集成测试。它封装了 Testcontainers 库来启动 Dapr 边车、控制平面服务(placement 和 scheduler)以及特定 Dapr 构建块所需的基础设施。

  • Dapr.Testcontainers(核心 harness 和基础设施)
  • Dapr.Testcontainers.Xunit(可选的 xUnit 辅助工具)

前置条件

  • 一个容器运行时(Docker Desktop、Podman 或等效工具)。
  • 能够拉取 Dapr 和依赖镜像的网络访问。

核心概念

Dapr.Testcontainers 围绕环境harness来建模测试:

  • DaprTestEnvironment:共享基础设施(网络、placement、scheduler、可选 Redis)。当你需要多个应用共享 Dapr 控制平面或运行多应用测试时使用它。
  • DaprHarnessBuilder:为特定构建块(工作流、作业、分布式锁或对话)创建 harness。
  • DaprTestApplicationBuilder:使用 harness 启动你的测试应用,并将 Dapr 端点连接到配置。

基本工作流测试示例

下面的示例反映了 .NET SDK 测试套件中的工作流集成测试,展示了典型设置:

var componentsDir = TestDirectoryManager.CreateTestDirectory("workflow-components");

await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(needsActorState: true);
await environment.StartAsync();

var harness = new DaprHarnessBuilder(componentsDir)
    .WithEnvironment(environment)
    .BuildWorkflow();

await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
    .ConfigureServices(builder =>
    {
        builder.Services.AddDaprWorkflowBuilder(opt =>
        {
            opt.RegisterWorkflow<TestWorkflow>();
        });
    })
    .BuildAndStartAsync();

using var scope = testApp.CreateScope();
var workflowClient = scope.ServiceProvider.GetRequiredService<DaprWorkflowClient>();

await workflowClient.ScheduleNewWorkflowAsync(nameof(TestWorkflow), input: 42);

配置 Dapr 运行时

DaprRuntimeOptions 让你控制 Dapr 镜像版本、App ID、日志级别和容器日志:

var options = new DaprRuntimeOptions("1.17.0")
    .WithAppId("test-app")
    .WithLogLevel(DaprLogLevel.Debug)
    .WithContainerLogs();

var harness = new DaprHarnessBuilder(componentsDir)
    .WithOptions(options)
    .BuildWorkflow();

你也可以通过 DAPR_RUNTIME_VERSION 环境变量全局设置运行时版本。

xUnit 辅助工具

如果你使用 xUnit,Dapr.Testcontainers.Xunit 包包含一个辅助特性,可在运行时版本过低时跳过测试:

[MinimumDaprRuntimeFact("1.17.0")]
public async Task RequiresLatestRuntime()
{
    // ...
}

后续步骤

  • 查看 Dapr .NET SDK 仓库中的集成测试项目(例如 test/Dapr.IntegrationTest.Workflow)。
  • 使用与你所测试的构建块匹配的 harness(工作流、作业、分布式锁、对话)。

9.4 - Dapr 源代码分析器和生成器

用于常见 Dapr 问题的代码分析器和修复

Dapr 支持日益丰富的可选 Roslyn 分析器和代码修复提供程序,用于检查代码中的代码质量问题。从 v1.16 版本开始,开发者可以在每个标准功能包的基础上,从 NuGet 安装额外的项目,从而在解决方案中启用这些分析器。

规则违规通常标记为 InfoWarning,因此如果分析器发现问题,不一定会中断构建。所有代码分析违规都带有前缀 “DAPR”,并通过该前缀后的数字进行唯一区分。

安装和配置分析器

在 v1.16 Dapr 版本发布后,以下包将可通过 NuGet 获取:

  • Dapr.Actors.Analyzers
  • Dapr.Jobs.Analyzers
  • Dapr.Workflow.Analyzers

在您希望运行分析器的每个项目中安装每个 NuGet 包。该包将作为项目依赖项安装,分析器将在您编写代码时或作为 CI/CD 构建的一部分运行。分析器会标记现有代码中的问题,并在构建项目时警告您新出现的问题。

我们的许多分析器都有关联的代码修复,可以自动纠正问题。如果您的 IDE 支持此功能,任何可用的代码修复都将作为代码中的内联菜单选项显示。

此外,我们的大多数分析器还应该报告代码中被识别为规则关键方面的特定语法所在的行号和列号。如果您的 IDE 支持,双击任何分析器警告应该直接跳转到违反分析器规则的代码部分。

抑制特定分析器

如果您希望阻止分析器对项目的某个特定部分进行检查,可以通过多种方式单独抑制其输出。有关在项目或文件中抑制分析器的更多信息,请阅读相关的 .NET 文档

禁用所有分析器

如果您希望在不移除任何提供分析器的包的情况下禁用项目中的所有分析器,请在 csproj 文件中将 EnableNETAnalyzers 属性设置为 false

可用的分析器

诊断 IDDapr 包类别严重性添加版本描述可用代码修复
DAPR1301Dapr.WorkflowUsageWarning1.16工作流类型未向依赖注入提供程序注册Yes
DAPR1302Dapr.WorkflowUsageWarning1.16工作流活动类型未向依赖注入提供程序注册Yes
DAPR1401Dapr.ActorsUsageWarning1.16Actor 计时器方法调用要求在类型上存在指定的回调方法No
DAPR1402Dapr.ActorsUsageWarning1.16Actor 类型未向依赖注入注册Yes
DAPR1403Dapr.ActorsInteroperabilityInfo1.16将 options.UseJsonSerialization 设置为 true 以支持与非 .NET actor 的互操作性Yes
DAPR1404Dapr.ActorsUsageWarning1.16调用 app.MapActorsHandlers 以映射 Dapr actor 的端点Yes
DAPR1501Dapr.JobsUsageWarning1.16Job 调用要求为 IEndpointRouteBuilder 上每个预期的作业设置和配置 MapDaprScheduledJobHandlerNo

分析器类别

以下是分析器可以分配到的各个有效类别,这些类别是按照 .NET 分析器使用的标准类别建模的:

  • Design
  • Documentation
  • Globalization
  • Interoperability
  • Maintainability
  • Naming
  • Performance
  • Reliability
  • Security
  • Usage

10 - 使用 Dapr .NET SDK 开发应用程序

Dapr .NET SDK 的部署集成

同时考虑多个服务

使用你喜欢的 IDE 或编辑器启动应用程序时,通常假设你只需要运行一个东西:你正在调试的应用程序。然而,开发微服务会挑战你思考本地开发流程时同时考虑多个服务。微服务应用程序包含多个你可能需要同时运行的服务,以及需要管理的依赖项(如状态存储)。

在开发流程中加入 Dapr 意味着你需要管理以下关注点:

  • 你想要运行的每个服务
  • 每个服务的 Dapr 边车
  • Dapr 组件和配置清单
  • 额外的依赖项,如状态存储
  • 可选:用于 Actor 的 Dapr Placement 服务

本文档假设你正在构建一个生产应用程序,并希望创建可重复且稳健的开发实践。此处提供的指导是通用的,适用于使用 Dapr 的任何 .NET 服务器应用程序(包括 Actor)。

管理组件

对于使用 Dapr 进行本地开发,你有两种主要方法来存储组件定义:

  • 使用默认位置(~/.dapr/components
  • 使用你自己的位置

在源代码仓库中创建一个文件夹来存储组件和配置,将使你能够对这些定义进行版本控制和共享。此处提供的指导假设你在应用程序源代码旁边创建了一个文件夹来存储这些文件。

开发选项

选择以下链接之一,了解你可以在本地开发场景中使用的工具。建议你熟悉其中的每一个,以了解 .NET SDK 提供的选项。

10.1 - 使用 Dapr CLI 进行 Dapr .NET SDK 开发

了解如何使用 Dapr CLI 进行本地开发

Dapr CLI

可以将本文视为 Docker 自托管 Dapr 指南 的 .NET 伴侣指南。

Dapr CLI 为您提供了一个良好的开发基础,它会初始化本地 Redis 容器、Zipkin 容器、placement 服务以及 Redis 的组件清单。这使您可以在全新安装且无需额外设置的情况下,使用以下构建块:

您可以使用 dapr run 来运行 .NET 服务,作为本地开发的策略。计划为每个服务运行以下命令之一以启动您的应用程序。

  • 优点: 由于这是默认 Dapr 安装的一部分,因此设置起来很容易
  • 缺点: 这会在您的机器上使用长时间运行的 Docker 容器,这可能并不理想
  • 缺点: 这种方法的可扩展性较差,因为它需要为每个服务单独运行命令

使用 Dapr CLI

对于每个服务,您需要选择:

  • 用于寻址的唯一 app-id(app-id
  • 用于 HTTP 的唯一监听端口(port

您还应该决定在哪里存储组件(components-path)。

以下命令可以在多个终端中运行以启动每个服务,并将相应的值替换进去。

dapr run --app-id <app-id> --app-port <port> --components-path <components-path> -- dotnet run -p <project> --urls http://localhost:<port>

说明: 此命令将使用 dapr run 来启动每个服务及其边车。命令的前半部分(在 -- 之前)将所需的配置传递给 Dapr CLI。命令的后半部分(在 -- 之后)将所需的配置传递给 dotnet run 命令。

如果您的任何服务不接受 HTTP 流量,则通过删除 --app-port--urls 参数来修改上述命令。

后续步骤

如果您需要调试,请使用调试器的附加功能附加到正在运行的进程之一。

如果您想扩展这种方法,请考虑构建一个脚本,为整个应用程序自动执行此过程。

10.2 - 使用 Docker Compose 进行 Dapr .NET SDK 开发

了解如何使用 Docker Compose 进行本地开发

Docker Compose

可将本文视为 使用 Docker 自托管 Dapr 指南 的 .NET 伴侣指南。

docker-compose 是随 Docker Desktop 附带的 CLI 工具,可用于同时运行多个容器。它是一种将多个容器的生命周期进行自动化的方式,并为面向 Kubernetes 的应用提供类似生产环境的开发体验。

  • 优点: 由于 docker-compose 会为你管理容器,你可以将依赖项作为应用定义的一部分,并停止机器上长期运行的容器。
  • 缺点: 投入成本最高,服务需要容器化才能开始使用。
  • 缺点: 如果你不熟悉 Docker,可能会难以调试和排查故障。

使用 docker-compose

从 .NET 的角度来看,对于 Dapr 的 docker-compose 不需要专门的指导。docker-compose 运行容器,一旦你的服务进入容器中,其配置与任何其他编程技术类似。

总结方法如下:

  • 为每个服务创建一个 Dockerfile
  • 创建一个 docker-compose.yaml 并将其签入源代码仓库

要了解如何编写 docker-compose.yaml,你应该从 Hello, docker-compose 示例 开始。

与使用 dapr run 在本地运行的每个服务类似,你需要选择一个唯一的 app-id。选择容器名称作为 app-id 会更容易记忆。

compose 文件至少包含:

  • 容器用于通信的网络
  • 每个服务的容器
  • 指定了服务端口和 app-id 的 <service>-daprd 边车容器
  • 在容器中运行的其他依赖项(例如 redis)
  • 可选:Dapr placement 容器(用于 Actor)

你还可以从 eShopOnContainers 示例应用中查看更大的示例。

10.3 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发

了解使用 .NET Aspire 进行本地开发

.NET Aspire

.NET Aspire 是一种开发工具, 旨在通过提供一个框架,使第三方服务能够与您自己的软件一起轻松集成、观察和配置,从而更轻松地将外部软件包含到 .NET 应用程序中。

Aspire 通过与流行的 IDE 提供丰富的集成来简化本地开发,包括 Microsoft Visual StudioVisual Studio CodeJetBrains Rider 等, 以便在启动调试器启动应用程序的同时,自动启动和配置对其他集成的访问,包括 Dapr。

虽然 Aspire 还协助将应用程序部署到各种云主机(如 Microsoft Azure 和 Amazon AWS),但部署目前超出了本指南的范围。更多信息可以在 Aspire 的文档这里找到。

可以在这里找到一个端到端演示,其中包含以下内容并演示了多个启用 Dapr 的服务之间的服务调用。

前提条件

  • Dapr .NET SDK 支持 .NET 8.NET 9.NET 10。使用支持您选择的运行时的 .NET Aspire 版本。
  • 符合 OCI 标准的容器运行时,例如 Docker DesktopPodman
  • 安装并初始化 Dapr v1.16 或更高版本

通过 CLI 使用 .NET Aspire

我们将首先创建一个全新的 .NET 应用程序。打开您喜欢的 CLI 并导航到您希望在其中创建新 .NET 解决方案的目录。首先使用以下命令安装一个模板,该模板将创建一个空的 Aspire 应用程序:

dotnet new install Aspire.ProjectTemplates

安装完成后,继续在当前目录中创建一个空的 .NET Aspire 应用程序。-n 参数允许您指定输出解决方案的名称。如果排除该参数,.NET CLI 将改为使用输出目录的名称,例如 C:\source\aspiredemo 将导致解决方案被命名为 aspiredemo。本教程的其余部分将假设解决方案名为 aspiredemo

dotnet new aspire -n aspiredemo

这将在您的目录中创建两个 Aspire 特定的目录和一个文件:

  • aspiredemo.AppHost/ 包含 Aspire 编排项目,用于配置应用程序中使用的每个集成。
  • aspiredemo.ServiceDefaults/ 包含一系列旨在您的解决方案中共享的扩展,以帮助 Aspire 提供的弹性、服务发现和遥测功能(这些与 Dapr 本身提供的功能不同)。
  • aspiredemo.sln 是维护当前解决方案布局的文件

接下来,我们将创建两个项目,作为我们的 Dapr 应用程序并演示 Dapr 功能。在同一目录中,使用以下命令创建一个名为 FrontEndApp 的空 ASP.NET Core 项目和另一个名为 ‘BackEndApp’ 的项目。任何一个都将在当前目录下的 FrontEndApp\FrontEndApp.csprojBackEndApp\BackEndApp.csproj 中创建。

dotnet new web --name FrontEndApp

接下来,我们将配置 AppHost 项目以添加必要的包以支持本地 Dapr 开发。使用以下命令导航到 AppHost 目录,并将 CommunityToolkit.Aspire.Hosting.Dapr 包从 NuGet 安装到项目中。

我们还将添加对 FrontEndApp 项目的引用,以便我们可以在注册过程中引用它。

cd aspiredemo.AppHost
dotnet add package CommunityToolkit.Aspire.Hosting.Dapr
dotnet add reference ../FrontEndApp/
dotnet add reference ../BackEndApp/

接下来,我们需要将 Dapr 配置为与您的项目一起加载的资源。在您喜欢的 IDE 中打开该项目中的 Program.cs 文件。它应该类似于以下内容:

var builder = DistributedApplication.CreateBuilder(args);

builder.Build().Run();

如果您熟悉 ASP.NET Core 项目或其他使用 Microsoft.Extensions.DependencyInjection 功能的项目中使用的依赖注入方法,您会发现这是一个熟悉的体验。

由于我们已经添加了对 MyApp 的项目引用,因此我们需要在此配置中首先添加一个引用。在 builder.Build().Run() 行之前添加以下内容:

var backEndApp = builder
    .AddProject<Projects.BackEndApp>("be")
    .WithDaprSidecar();

var frontEndApp = builder
    .AddProject<Projects.FrontEndApp>("fe")
    .WithDaprSidecar();

由于项目引用已添加到此解决方案,您的项目在此处作为 Projects. 命名空间中的类型显示。在此教程中,为项目分配的变量名称并不重要,但如果您想使用 Aspire 的服务发现功能在此项目与另一个项目之间创建引用,则将使用该名称。

添加 .WithDaprSidecar() 将 Dapr 配置为 .NET Aspire 资源,以便当项目运行时,边车将与您的应用程序一起部署。这接受许多不同的选项,并且可以按照以下示例进行配置:

DaprSidecarOptions sidecarOptions = new()
{
    AppId = "how-dapr-identifies-your-app",
    AppPort = 8080, //请注意,如果您打算从 Aspire v9.0 开始配置发布订阅、actor 或工作流,则需要此参数
    DaprGrpcPort = 50001,
    DaprHttpPort = 3500,
    MetricsPort = 9090
};

builder
    .AddProject<Projects.BackEndApp>("be")
    .WithReference(myApp)
    .WithDaprSidecar(sidecarOptions);

最后,让我们向后端应用添加一个端点,我们可以使用 Dapr 的服务调用调用它以显示到页面以演示 Dapr 正在按预期工作。

当您在 IDE 中打开解决方案时,请确保 aspiredemo.AppHost 配置为您的启动项目,但是当您以调试配置启动它时,您会注意到您的集成控制台应该反映您预期的 Dapr 日志,并且它将对您的应用程序可用。

11 - 如何使用 Dapr .NET SDK 进行故障排除和调试

使用 Dapr .NET SDK 进行故障排除和调试的提示、技巧和指南

11.1 - 使用 .NET SDK 对发布订阅进行故障排查

使用 .NET SDK 对发布订阅进行故障排查

发布订阅故障排查

发布订阅最常见的问题是应用程序中的发布订阅端点未被调用。

这个问题有几个层次,对应不同的解决方案:

  • 应用程序未接收到来自 Dapr 的任何流量
  • 应用程序未向 Dapr 注册发布订阅端点
  • 发布订阅端点已向 Dapr 注册,但请求未到达预期的端点

步骤 1:提高日志级别

这很重要。后续步骤将取决于你查看日志输出的能力。ASP.NET Core 在默认日志设置下几乎不输出任何内容,因此你需要更改它。

按照此处的说明,调整日志详细程度以包含 ASP.NET Core 的 Information 级别日志。将 Microsoft 键设置为 Information

步骤 2:验证你可以接收来自 Dapr 的流量

  1. 按正常方式启动应用程序(dapr run ...)。确保你在命令行中包含了 --app-port 参数。Dapr 需要知道你的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序在本地开发中会在端口 5000 上监听 HTTP。

  2. 等待 Dapr 完成启动

  3. 检查日志

你应该会看到类似以下的日志条目:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/.....

在初始化期间,Dapr 会向你的应用程序发出一些配置请求。如果你找不到这些请求,说明出现了问题。请通过 issue 或 Discord 寻求帮助(并附上日志)。如果你看到向你的应用程序发出的请求,则继续执行步骤 3。

步骤 3:验证端点注册

  1. 按正常方式启动应用程序(dapr run ...)。

  2. 在命令行使用 curl(或其他 HTTP 测试工具)访问 /dapr/subscribe 端点。

以下是一个示例命令,假设你的应用程序监听端口是 5000:

curl http://localhost:5000/dapr/subscribe -v

对于正确配置的应用程序,输出应如下所示:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /dapr/subscribe HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 15 Jan 2021 22:31:40 GMT
< Content-Type: application/json
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
[{"topic":"deposit","route":"deposit","pubsubName":"pubsub"},{"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}]* Closing connection 0

特别注意 HTTP 状态码和 JSON 输出。

< HTTP/1.1 200 OK

200 状态码表示成功。

末尾包含的 JSON 数据块是 /dapr/subscribe 的输出,由 Dapr 运行时处理。在本例中,它使用的是此仓库中的 ControllerSample - 因此这是正确输出的示例。

[
    {"topic":"deposit","route":"deposit","pubsubName":"pubsub"},
    {"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}
]

掌握了此命令的输出后,你就可以诊断问题或继续下一步。

选项 0:响应是 200 且包含一些发布订阅条目

如果此测试的 JSON 输出中有条目,则问题出在其他地方,请继续执行步骤 2。

选项 1:响应不是 200,或不包含 JSON

如果响应不是 200 或不包含 JSON,则表示未到达 MapSubscribeHandler() 端点。

确保你在 Startup.cs 中有类似以下的代码,然后重复测试。

app.UseRouting();

app.UseCloudEvents();

app.UseEndpoints(endpoints =>
{
    endpoints.MapSubscribeHandler(); // This is the Dapr subscribe handler
    endpoints.MapControllers();
});

如果添加订阅处理程序未能解决问题,请在此仓库上提交 issue 并包含你的 Startup.cs 文件的内容。

选项 2:响应包含 JSON 但为空(如 []

如果 JSON 输出是空数组(如 []),则表示订阅处理程序已注册,但未注册任何主题端点。


如果你使用控制器进行发布订阅,你应该有一个类似以下的方法:

[Topic("pubsub", "deposit")]
[HttpPost("deposit")]
public async Task<ActionResult> Deposit(...)

// Using Pub/Sub routing
[Topic("pubsub", "transactions", "event.type == \"withdraw.v2\"", 1)]
[HttpPost("withdraw")]
public async Task<ActionResult> Withdraw(...)

在此示例中,TopicHttpPost 特性是必需的,但其他细节可能不同。


如果你使用路由进行发布订阅,你应该有一个类似以下的端点:

endpoints.MapPost("deposit", ...).WithTopic("pubsub", "deposit");

在此示例中,调用 WithTopic(...) 是必需的,但其他细节可能不同。


更正此代码并重新测试后,如果 JSON 输出仍然是空数组(如 []),请在此仓库上提交 issue 并包含 Startup.cs 的内容和你的发布订阅端点。

步骤 4:验证端点可达性

在此步骤中,我们将验证向发布订阅注册的条目是否可访问。上一步应该会给你一些类似以下的 JSON 输出:

[
  {
    "pubsubName": "pubsub",
    "topic": "deposit",
    "route": "deposit"
  },
  {
    "pubsubName": "pubsub",
    "topic": "deposit",
    "routes": {
      "rules": [
        {
          "match": "event.type == \"withdraw.v2\"",
          "path": "withdraw"
        }
      ]
    }
  }
]

保留此输出,因为我们将使用 route 信息来测试应用程序。

  1. 按正常方式启动应用程序(dapr run ...)。

  2. 在命令行使用 curl(或其他 HTTP 测试工具)访问向发布订阅端点注册的路由之一。

以下是一个示例命令,假设你的应用程序监听端口是 5000,且你的发布订阅路由之一是 withdraw

curl http://localhost:5000/withdraw -H 'Content-Type: application/json' -d '{}' -v

以下是针对示例运行上述命令的输出:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> POST /withdraw HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 2
>
* upload completely sent off: 2 out of 2 bytes
< HTTP/1.1 400 Bad Request
< Date: Fri, 15 Jan 2021 22:53:27 GMT
< Content-Type: application/problem+json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|5e9d7eee-4ea66b1e144ce9bb.","errors":{"Id":["The Id field is required."]}}* Closing connection 0

根据 HTTP 400 和 JSON 负载,此响应表示已到达端点,但由于验证错误而拒绝了请求。

你还应该查看运行中应用程序的控制台输出。这是示例输出,为清晰起见,去除了 Dapr 日志头。

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 POST http://localhost:5000/withdraw application/json 2
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
      Route matched with {action = "Withdraw", controller = "Sample"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[ControllerSample.Account]] Withdraw(ControllerSample.Transaction, Dapr.Client.DaprClient) on controller ControllerSample.Controllers.SampleController (ControllerSample).
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
      Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ValidationProblemDetails'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
      Executed action ControllerSample.Controllers.SampleController.Withdraw (ControllerSample) in 52.1211ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 157.056ms 400 application/problem+json; charset=utf-8

主要感兴趣的日志条目是来自路由的条目:

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'

此条目显示:

  • 路由已执行
  • 路由选择了 ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)' 端点

现在你拥有了对此步骤进行故障排查所需的信息。

选项 0:路由选择了正确的端点

如果路由日志条目中的信息正确,则表示你的应用程序在隔离状态下运行正常。

示例:

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'ControllerSample.Controllers.SampleController.Withdraw (ControllerSample)'

你可能希望尝试使用 Dapr CLI 直接发送发布订阅消息并比较日志输出。

示例命令:

dapr publish --pubsub pubsub --topic withdraw --data '{}'

如果执行此操作后你仍不理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。

选项 1:路由未执行

如果你在日志中未看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,则表示请求由路由以外的其他组件处理。在这种情况下,问题通常是中间件行为异常。请求的其他日志可能会给你一些线索,让你了解正在发生什么。

如果你需要帮助理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。

选项 2:路由选择了错误的端点

如果你在日志中看到 Microsoft.AspNetCore.Routing.EndpointMiddleware 的条目,但它包含错误的端点,则表示你存在路由冲突。所选端点将出现在日志中,这应该会让你了解是什么导致了冲突。

如果你需要帮助理解问题,请在此仓库上提交 issue 并包含你的 Startup.cs 的内容。