Dapr 软件开发工具包(SDK) 使用您喜欢的语言与 Dapr 一起工作
Dapr SDK 是将 Dapr 集成到应用程序的最简单方式。选择您喜欢的语言,几分钟内即可上手使用 Dapr。
SDK 包 选择下方的您偏好的语言 ,以了解更多关于客户端、服务端、Actor 和工作流包的信息。
客户端(Client) :Dapr 客户端允许您调用 Dapr 构建块 API 并执行各构建块的操作服务扩展(Server extensions) :Dapr 服务扩展允许您创建可被其他服务调用并可订阅主题的服务Actor :Dapr Actor SDK 允许您构建包含方法、状态、定时器和持久化提醒的虚拟 Actor工作流(Workflow) :Dapr 工作流让您能够以可靠的方式轻松编写长时间运行的业务逻辑和集成SDK 支持的语言 框架 框架 语言 状态 描述 Dapr Agents Python In development 一个用于构建由大语言模型(LLM)驱动的自主代理的框架,该框架利用 Dapr 的分布式系统能力实现可靠执行,并内置安全性、可观测性和状态管理。
延伸阅读 1 - Dapr .NET SDK 用于开发 Dapr 应用程序的 .NET SDK 包
Dapr 提供了多种包来帮助开发 .NET 应用程序。使用它们,您可以创建用于 Dapr 的 .NET 客户端、服务器和虚拟 actor。
前置条件
注意 Dapr .NET SDK 支持 .NET 8、.NET 9 和 .NET 10。.NET 8 和 .NET 9 将持续支持至其生命周期结束(2026 年 11 月)之后的第一个 Dapr 版本,之后我们预计将转而支持 .NET 10 和 .NET 11。安装 要开始使用 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 协作以实现发布订阅应用程序。
可用包 更多信息 了解有关本地开发选项、最佳实践的更多信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。
1.1 - Dapr 客户端 .NET SDK 入门 如何开始使用 Dapr .NET SDK
Dapr 客户端包允许你从 .NET 应用程序与其他 Dapr 应用程序进行交互。
注意 如果尚未尝试,请尝试
快速入门之一 ,快速了解如何使用 API 构建块结合 Dapr .NET SDK。
构建块 .NET SDK 允许你与所有 Dapr 构建块 进行交互。
注意 我们只会在第一个示例(服务调用)中包含 DaprClient 的依赖注入注册。在几乎所有其他示例中,我们假设你已经在后续示例的应用程序中注册了 DaprClient,并且已将 DaprClient 的实例作为名为 client 的实例注入到代码中。调用服务 HTTP 你可以使用 DaprClient 或 System.Net.Http.HttpClient 来调用服务。
ASP.NET Core 项目
控制台项目
HTTP 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 = 99 m };
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 = 99 m };
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 = 10 m , };
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.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 ));
});
另一个可选的配置委托重载提供了对 DaprClientBuilder 和 IServiceProvider 的访问,允许进行更高级的配置,这些配置可能需要从依赖注入容器中注入服务。
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 实例时使用已注册的 IHttpClientFactoryUseJsonSerializationOptions(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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,示例:https://dapr-grpc-api.mycompany.comDAPR_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 令牌
注意 如果同时指定了 DAPR_HTTP_ENDPOINT 和 DAPR_HTTP_PORT,则将忽略 DAPR_HTTP_PORT 中的端口值,优先使用 DAPR_HTTP_ENDPOINT 上隐式或显式定义的端口。对于 DAPR_GRPC_ENDPOINT 和 DAPR_GRPC_PORT 同理。作为一般规则,应同时指定 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 属性检查更多异常详细信息。
1.2 - Dapr Workflow .NET SDK 使用 Dapr Workflow 和 Dapr .NET SDK 快速上手
1.2.1 - DaprWorkflowClient 生命周期管理与注册 了解如何配置 DaprWorkflowClient 生命周期管理与依赖注入
生命周期管理 DaprWorkflowClient 掌控着用于与 Dapr 边车通信的 TCP 套接字形式持有的网络资源,以及其他用于工作流管理和操作的类型。DaprWorkflowClient 实现了 IAsyncDisposable 以支持资源的主动清理。
依赖注入 AddDaprWorkflow() 方法会将 Dapr 工作流服务注册到 ASP.NET Core 的依赖注入容器中。这个方法需要一个选项委托,用于定义你希望在应用中注册和使用的每个工作流和活动。
注意 此方法将尝试注册 DaprClient 实例,但仅在该实例尚未被其他生命周期注册时才会成功。例如,若之前已通过 AddDaprClient() 注册为单例生命周期,则无论为工作流客户端选择何种生命周期,都将始终使用该单例。DaprClient 实例将用于与 Dapr 边车通信;如果尚未注册,则 AddDaprWorkflow() 注册时提供的生命周期将同时用于注册 DaprWorkflowClient 及其自身依赖项。修改 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 轻松添加日志记录,或通过注入 DaprClient 或 DaprJobsClient 访问其他 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 );
//...
}
}
后续步骤 1.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 运行时。
注意 此功能需要 Dapr .NET SDK v1.17.0 或更高版本。兼容性和重大变更 警告 更改序列化可能是现有工作流的重大变更。序列化实现之间没有支持的迁移路径。
所有 Dapr SDK 默认使用标准 JSON 约定。如果您在 .NET 工作流和活动中更改序列化设置或切换到自定义序列化器,跨 SDK 工作流可能会失败,因为其他 SDK 可能不支持您的自定义序列化格式。
默认 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 );
});
1.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 ;
}
}
注意 当在另一个应用程序中调用工作流时,你需要使用该应用程序期望的工作流名称。
如果目标应用程序是使用命名工作流版本控制的 .NET 应用程序,你可以通过其规范(未版本化)工作流名称调用它,目标应用程序会将其路由到最新版本。命名工作流版本控制需要 Dapr 运行时
v1.17.0 或更高版本(多应用程序工作流仅需要 v1.16.0+)。后续步骤 1.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 );
后续步骤 1.2.5 - .NET SDK 中的工作流版本控制 了解如何在 Dapr .NET SDK 中使用基于补丁和基于名称的工作流版本控制
概述 Dapr 工作流版本控制允许你演进工作流,而不会中断进行中实例的确定性执行。
.NET SDK 支持两种方法:
基于补丁的版本控制 :引入由 context.IsPatched("patch-name") 守护的条件分支。基于名称的版本控制 :创建一个新的工作流类型名称,并让版本控制策略选择最新版本。对于小型就地更改,使用基于补丁的版本控制。对于需要全新的工作流类型的大型重构,使用基于名称的版本控制。
注意 工作流版本控制需要 Dapr .NET SDK v1.17.0 或更高版本以及 Dapr 运行时 v1.17.0 或更高版本。何时使用每种方法 基于补丁的版本控制 适用于以下情况:
你需要在现有工作流中进行小型、增量式的更改。 你希望现有实例在部署后保持确定性行为。 你希望暂时避免引入新的工作流类型。 基于名称的版本控制 适用于以下情况:
你想要一个没有累积补丁的全新工作流类型。 你准备好删除旧的补丁块并更自由地重构。 你希望基于命名约定自动选择版本。 基于补丁的版本控制 基于补丁的版本控制依赖于工作流内部的确定性开关。使用 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")以避免非确定性行为。 IsPatched 在 WorkflowContext 上可用;无需额外设置。
提示 保持补丁名称简单且单调(例如,"v2"、"v3"),以便清楚了解每次部署引入的更改。基于名称的版本控制 基于名称的版本控制允许你通过更改工作流类型名称来创建新版本的工作流。推荐的模式是将现有工作流复制到新文件中,重命名类,根据需要进行重构,然后在必要时再次开始打补丁。
例如,如果你有 OrderWorkflow,则创建 OrderWorkflowV2 并重构它。较旧的版本可以保留给进行中的实例,而新实例使用最新版本。
默认命名行为 默认情况下,基于名称的版本控制使用内置的 NumericVersionStrategy 和数字后缀。以下都是有效示例:
MyWorkflow(被视为版本 0)MyWorkflow2MyWorkflowV2默认策略假设较高的数字值是较新的版本(例如,MyWorkflowV10 比 MyWorkflowV2 更新)。.NET SDK 还包含其他内置策略(Date、SemVer 和 Numeric)以及对自定义策略的支持。
内置策略和选项 .NET SDK 附带了几个内置的基于名称的策略。每个策略都支持允许你调整如何解析后缀以及当没有后缀时该如何处理的选项。
DateVersionStrategy :从尾随后缀派生基于日期的版本(例如,MyWorkflow20220611)。
选项包括:日期格式 :使用标准 C# 日期格式规则;默认为 yyyyMMdd。默认版本 :当未提供后缀时使用;默认为 0。前缀 :在日期后缀之前匹配的可选前缀,带有可选区分大小写。SemVerVersionStrategy :从尾随后缀派生 SemVer 版本(例如,MyWorkflow1.2.3)。
选项包括:前缀 :在 SemVer 后缀之前匹配的可选前缀,带有可选区分大小写。预发布/生成支持 :可以解析预发布注释和生成元数据。默认版本 :当未提供后缀时的可选默认值(如果配置为允许缺少后缀)。NumericVersionStrategy :从尾随后缀派生数字版本(例如,MyWorkflow42 或
MyWorkflowV42)。
选项包括:前缀 :在数字后缀之前匹配的可选前缀,带有可选区分大小写。零填充宽度 :可选宽度,允许使用带前导零的固宽数字。默认版本 :当未提供后缀时使用。配置基于名称的版本控制 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 中找到。
后续步骤 1.2.6 - .NET 工作流示例 探索 GitHub 上的 Dapr 工作流代码示例
Dapr Quickstarts 仓库中的工作流教程 GitHub 上的 Dapr Quickstarts 仓库包含许多工作流教程,展示各种工作流模式以及如何使用工作流管理操作。你可以在 quickstarts/tutorials/workflow/csharp 文件夹中找到这些教程。
.NET SDK 仓库中的工作流示例 GitHub 上的 Dapr .NET SDK 仓库包含多个示例,演示如何将 Dapr 工作流与 .NET 结合使用。你可以在 examples/Workflow 文件夹中找到这些示例。
后续步骤 1.3 - Dapr Actors .NET SDK 快速上手 Dapr Actors .NET SDK
通过 Dapr Actor 包,您可以从 .NET 应用程序与 Dapr 虚拟 Actor 进行交互。
要开始使用,请参阅 Dapr Actors 操作指南。
1.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。
提示 对于非依赖注入的应用程序,你可以使用 ActorProxy 上的静态方法。由于 ActorProxy 方法容易出错,在配置自定义设置时尽量避免使用它们。标识 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 在调用方和被调用方之间匹配异常。以下是异常详细信息的示例:
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 。
1.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 可以实现 IDisposable 或 IAsyncDisposable。建议依赖依赖注入进行资源管理,而不是在应用程序代码中实现 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.cs 中 ConfigureServices 的一部分。你可以通过 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 ();
});
}
UseRouting 和 UseEndpoints 调用是配置路由所必需的。通过在终结点中间件内添加 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 示例 。
1.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] 的更多信息和示例,请参阅官方文档 。
1.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 服务项目 (\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 方法的返回类型必须是 Task 或 Task<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}" );
}
}
}
运行代码 你现在可以测试你创建的项目。
运行 MyActorService
由于 MyActorService 承载着 actors,它需要使用 Dapr CLI 运行。
cd MyActorService
dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run
你将在此终端中看到来自 daprd 和 MyActorService 的命令行输出。你应该会看到类似以下内容,这表明应用程序已成功启动。
...
ℹ️ 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
运行 MyActorClient
MyActorClient 充当客户端,可以使用 dotnet run 正常运行。
打开一个新终端并导航到 MyActorClient 目录。然后使用以下命令运行项目:
你应该会看到类似以下的命令行输出:
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 服务和客户端。请参阅相关链接部分以了解更多信息。
相关链接 1.4 - Dapr AI .NET SDK 快速上手 Dapr AI .NET SDK
使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。
目前,Dapr 提供对话 API 来与大语言模型进行交互。要开始使用此工作负载,请阅读 Dapr 对话 AI 操作指南。
1.4.1 - Dapr AI 客户端 了解如何创建 Dapr AI 客户端
Dapr AI 客户端包允许您与 Dapr 边车提供的 AI 功能进行交互。
注意 Dapr Conversation 构建块需要 Dapr 运行时 v1.16.0 或更高版本。响应格式、提示缓存保留以及令牌使用统计需要 Dapr 运行时 v1.17.0 或更高版本。生命周期管理 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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_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 ();
发送对话请求 使用 ConversationInput 和 ConversationOptions 向您的对话组件发送提示:
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 ();
1.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 交互,以从大型语言模型发送和接收消息。
1.4.3 - 如何:在 Dapr 的 .NET Conversation SDK 中使用 Microsoft 的 AI 扩展 了解如何创建和使用结合 Microsoft AI 扩展的 Dapr
前置条件 安装 要开始使用此 SDK,请从 NuGet 安装 Dapr.AI 和
Dapr.AI.Microsoft.Extensions 包:
dotnet add package Dapr.AI
dotnet add package Dapr.AI.Microsoft.Extensions
DaprChatClient 是 IChatClient 接口的基于 Dapr 的实现,该接口由
Microsoft.Extensions.AI.Abstractions 包提供,使用 Dapr 的[对话构建块]({{ ref conversation-overview.md }})。它允许
开发者针对 Microsoft 提供的抽象类型进行开发,同时提供与 Dapr 对话构建块的最大一致性。由于这两种方法都采用 OpenAI 的 API 方式,
预计它们在未来会日益趋同。
Dapr 对话构建块 请注意,Dapr 的对话构建块仍处于 alpha 状态,这意味着 API 的形状
可能会在未来版本中发生变化。此 SDK 包的目的是提供一个与 Microsoft 的 AI 扩展对齐且同时映射到并符合 Dapr API 的 API,
但类型和属性的名称可能会从一个版本更改为下一个版本,因此在使用此 SDK 时请注意这种可能性。关于 Microsoft.Extensions.AI Dapr.AI.Microsoft.Extensions 包实现了 Microsoft.Extensions.AI 抽象,为
.NET 应用程序中的 AI 服务提供统一的 API。Microsoft.Extensions.AI 旨在为
不同的 AI 提供商和场景提供一致的编程模型。有关 Microsoft.Extensions.AI 的详细信息,请参阅
官方文档 。
有限支持 请注意,Microsoft 的 AI 扩展提供的属性和方法比 Dapr 的对话构建块当前
支持的要多得多。此包将仅映射那些具有 Dapr 支持的属性,而忽略其他属性,因此仅仅
它在 Microsoft.Extensions.AI 包中可用并不意味着 Dapr 支持它。请依赖此文档
和包中公开的 XML 文档来了解什么支持、什么不支持。服务注册 可以使用多个扩展方法将 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 在调用对话组件时将遵循这些设置。
最佳实践 服务生命周期 :为 DaprChatClient 注册使用 ServiceLifetime.Scoped 或 ServiceLifetime.Singleton,以避免不必要地创建多个实例。
错误处理 :始终将调用包装在适当的 try-catch 块中,以处理 Dapr 特定和常规异常。
资源管理 :DaprChatClient 通过其基类正确实现了 IDisposable,因此在使用依赖注入时会自动管理资源。
配置 :正确配置您的 Dapr 对话组件以确保最佳性能和可靠性。
相关链接 1.5 - Dapr Jobs .NET SDK 通过 Dapr Jobs 和 Dapr .NET SDK 快速上手
使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 交互,按照预定义的计划触发未来操作运行,并支持可选的有效负载。
要开始使用,请阅读 Dapr Jobs 操作指南,并参考最佳实践文档 获取更多指导。
1.5.1 - 操作指南:在 .NET SDK 中编写和管理 Dapr Jobs 了解如何使用 .NET SDK 编写和管理 Dapr Jobs
让我们创建一个在 Dapr Jobs 触发时会被调用的端点,然后在同一应用中调度该任务。我们将使用此处提供的简单示例 进行以下演示,并以此作为说明,介绍如何使用间隔时间或 Cron 表达式来调度一次性或重复任务。在本指南中,你将:
部署一个 .NET Web API 应用程序 (JobsSample ) 使用 Dapr .NET Jobs SDK 来调度任务调用并设置要触发的端点 在 .NET 示例项目中:
前置条件 设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录导航到 Dapr Jobs 示例。
在本地运行应用程序 要运行 Dapr 应用程序,你需要启动 .NET 程序和 Dapr 边车。导航到 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 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfiguration,AddDaprJobsClient() 注册将自动使用这些值而不是各自的默认值。
首先,在配置中填充值。这可以通过几种不同的方式来完成,如下所示。
通过 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_ENDPOINT http://localhost:54321 DAPR_API_TOKEN abc123
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ();
builder . Services . AddDaprJobsClient ();
Dapr Jobs 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
通过带前缀的环境变量进行配置 然而,在多个应用程序在同一台机器上运行而不使用容器的共享主机场景或开发环境中,为环境变量添加前缀并不少见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:
键 值 myapp_DAPR_HTTP_ENDPOINT http://localhost:54321 myapp_DAPR_API_TOKEN abc123
这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。
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 方法都支持取消令牌,如果未另外设置,则使用默认令牌。
有三种不同的方式来设置任务,具体取决于你想如何配置调度。以下显示了调度任务时可用的不同参数:
参数名称 类型 描述 必填 jobName string 正在调度的任务的名称。 是 schedule DaprJobSchedule 定义任务何时触发的调度。 是 payload ReadOnlyMemory 在触发时提供给调用端点的任务数据。 否 startingFrom DateTime 任务调度应开始的时间点。 否 repeats int 任务应触发的最大次数。 否 ttl 任务何时应过期且不再触发。 否 overwrite bool 一个标志,指示提交时是否应覆盖现有任务,如果为 false,则需要先删除同名的现有任务。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
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 );
}
}
1.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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_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!" );
处理作业调用的委托期望至少存在两个参数:
一个填充了 jobName 的 string,提供被调用作业的名称 一个填充了作业注册期间最初提供的字节的 ReadOnlyMemory<byte> 由于负载存储为 ReadOnlyMemory<byte>,开发人员可以自由地序列化和反序列化,但同样包含两个辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。两种方法都假定开发人员对最初调度的作业进行了编码(可能使用了辅助序列化方法),因为这些方法不会强制字节表示它们不是的内容。
要将字节反序列化为字符串,可以使用以下辅助方法:
var payloadAsString = Encoding . UTF8 . GetString ( jobPayload . Span ); // 如果成功,则返回包含值的字符串
错误处理 如果在 SDK 和运行在 Dapr 边车上的 Jobs API 服务之间遇到问题,DaprJobsClient 上的方法将抛出 DaprJobsServiceException。如果由于通过此 SDK 向 Jobs API 服务发出的格式错误的请求而导致失败,将抛出 DaprMalformedJobException。在非法参数值的情况下,将抛出相应的标准异常(例如 ArgumentOutOfRangeException 或 ArgumentNullException)以及违规参数的名称。对于其他任何情况,将抛出 DaprException。
最常见的失败情况与以下内容相关:
与 Jobs API 交互时参数格式不正确 瞬态故障,例如网络问题 无效数据,例如无法将值反序列化回最初未序列化的类型 在任何这些情况下,您都可以通过 .InnerException 属性检查更多异常详细信息。
1.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/) 操作指南。
1.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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_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 ();
1.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 存储库以尝试一些示例并开始使用。
1.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 操作指南,并参考最佳实践文档 获取更多指导。
1.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 显式指定每个值覆盖。相反,通过填充通过依赖注入提供的 IConfiguration,AddDaprPubSubClient() 注册将自动使用这些值而不是其各自的默认值。
首先填充配置中的值。可以通过以下几种不同方式来完成此操作,如下所示。
通过 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_ENDPOINT http://localhost:54321 DAPR_API_TOKEN abc123
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ();
builder . Services . AddDaprPubSubClient ();
Dapr PubSub 客户端将被配置为使用 HTTP 端点 http://localhost:54321,并在所有出站请求中填充 API 令牌头 abc123。
通过带前缀的环境变量配置 但是,在不使用容器的共享主机场景中(在同一台机器上运行多个应用程序)或在开发环境中,为环境变量添加前缀并不罕见。以下示例假定 HTTP 端点和 API 令牌都将从带有值 “myapp_” 前缀的环境变量中提取。在此场景中使用的两个环境变量如下:
键 值 myapp_DAPR_HTTP_ENDPOINT http://localhost:54321 myapp_DAPR_API_TOKEN abc123
这些环境变量将在以下示例中加载到已注册的配置中,并在不带前缀的情况下可用。
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 秒。
1.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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,示例:https://dapr-grpc-api.mycompany.comDAPR_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 ();
1.8 - Dapr 分布式锁 .NET SDK 快速上手 Dapr 分布式锁 .NET SDK
使用 Dapr 分布式锁包,您可以在资源上创建和移除锁,以管理分布式应用程序之间的独占性。
虽然此功能在 Dapr.Client 和 Dapr.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.comDAPR_GRPC_ENDPOINT:用于查找 Dapr 边车的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.comDAPR_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 ();
1.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 交互,以放置和移除锁,从而管理分布式应用程序中的资源独占性。
1.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 运行集成测试详见此处 。
1.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。
RetryInfo
DebugInfo
QuotaFailure
PreconditionFailure
RequestInfo
LocalizedMessage
BadRequest
ErrorInfo
Help
ResourceInfo
Unknown
RetryInfo 通知客户端在重试之前应等待多长时间的信息。提供一个带有 Second(秒偏移量)和 Nano(纳秒偏移量)属性的
DaprRetryDelay。
DebugInfo 服务器提供的调试信息。包含 StackEntries(包含堆栈跟踪的字符串集合)和 Detail(进一步的调试信息)。
QuotaFailure 与可能已达到的配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations,
DaprQuotaFailureViolation 的集合,每个集合包含一个 Subject(请求的主题)和 Description(有关失败的更多信息)。
PreconditionFailure 通知客户端某些必需的前提条件未满足的信息。有一个属性 Violations,DaprPreconditionFailureViolation 的集合,
每个集合具有 Subject(前提条件失败发生的主题,例如 “Azure”)、Type(前提条件类型的表示,例如 “TermsOfService”)和
Description(进一步描述,例如 “ToS must be accepted.")。
RequestInfo 服务器返回的信息,服务器可以使用该信息来标识客户端的请求。包含 RequestId 和 ServingData 属性,
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”)。
1.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] 来标记尚未通过稳定生命周期认证的构建块和组件。这种方法提供了更清晰的区分:
Experimental APIs — 已经可用但仍在演进的功能,尚未根据 Dapr Component Certification Lifecycle 认证为稳定。
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] 的方法或类时,应根据属性消息中提供的迁移指导计划迁移。
最佳实践 在应用程序代码中:
谨慎使用实验性 API,因为它们可能会在未来版本中发生变化 考虑将实验性 API 的使用隔离,以便将来更容易更新 记录您对实验性 API 的使用,以便团队了解 在测试代码中:
使用项目级抑制以避免在测试代码中充斥警告抑制 定期审查您使用的实验性 API,并检查它们是否已稳定 在为 SDK 做贡献时:
对于尚未完成认证的新构建块使用 [Experimental] 仅对真正被弃用的 API 使用 [Obsolete] 在 UrlFormat 参数中提供清晰的文档链接 其他资源 1.9.3 - Integration testing with Dapr.Testcontainers 使用 Dapr.Testcontainers 针对真实基础设施运行 Dapr 集成测试
概述 Dapr.Testcontainers 是一个辅助包,用于使用容器针对真实的 Dapr 运行时组件编写集成测试。它封装了 Testcontainers 库来启动 Dapr 边车、控制平面服务(placement 和 scheduler)以及特定 Dapr 构建块所需的基础设施。
重要 Dapr.Testcontainers 是初始版本。API 仍在演进中,可能在未来的版本中发生变化。我们尽量保持变化最小,但在该包成熟的过程中,你应预期可能出现破坏性变更。包 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(工作流、作业、分布式锁、对话)。 1.9.4 - Dapr 源代码分析器和生成器 用于常见 Dapr 问题的代码分析器和修复
Dapr 支持日益丰富的可选 Roslyn 分析器和代码修复提供程序,用于检查代码中的代码质量问题。从 v1.16 版本开始,开发者可以在每个标准功能包的基础上,从 NuGet 安装额外的项目,从而在解决方案中启用这些分析器。
Note Dapr .NET SDK 的未来版本将默认包含这些分析器,无需单独安装包。规则违规通常标记为 Info 或 Warning,因此如果分析器发现问题,不一定会中断构建。所有代码分析违规都带有前缀 “DAPR”,并通过该前缀后的数字进行唯一区分。
Note 目前,诊断标识符的前两位数字与不同的 Dapr 包一一对应,但随着更多分析器的开发,这一映射方式未来可能会发生变化。安装和配置分析器 在 v1.16 Dapr 版本发布后,以下包将可通过 NuGet 获取:
Dapr.Actors.Analyzers Dapr.Jobs.Analyzers Dapr.Workflow.Analyzers 在您希望运行分析器的每个项目中安装每个 NuGet 包。该包将作为项目依赖项安装,分析器将在您编写代码时或作为 CI/CD 构建的一部分运行。分析器会标记现有代码中的问题,并在构建项目时警告您新出现的问题。
我们的许多分析器都有关联的代码修复,可以自动纠正问题。如果您的 IDE 支持此功能,任何可用的代码修复都将作为代码中的内联菜单选项显示。
此外,我们的大多数分析器还应该报告代码中被识别为规则关键方面的特定语法所在的行号和列号。如果您的 IDE 支持,双击任何分析器警告应该直接跳转到违反分析器规则的代码部分。
抑制特定分析器 如果您希望阻止分析器对项目的某个特定部分进行检查,可以通过多种方式单独抑制其输出。有关在项目或文件中抑制分析器的更多信息,请阅读相关的 .NET 文档 。
禁用所有分析器 如果您希望在不移除任何提供分析器的包的情况下禁用项目中的所有分析器,请在 csproj 文件中将 EnableNETAnalyzers 属性设置为 false。
可用的分析器 诊断 ID Dapr 包 类别 严重性 添加版本 描述 可用代码修复 DAPR1301 Dapr.Workflow Usage Warning 1.16 工作流类型未向依赖注入提供程序注册 Yes DAPR1302 Dapr.Workflow Usage Warning 1.16 工作流活动类型未向依赖注入提供程序注册 Yes DAPR1401 Dapr.Actors Usage Warning 1.16 Actor 计时器方法调用要求在类型上存在指定的回调方法 No DAPR1402 Dapr.Actors Usage Warning 1.16 Actor 类型未向依赖注入注册 Yes DAPR1403 Dapr.Actors Interoperability Info 1.16 将 options.UseJsonSerialization 设置为 true 以支持与非 .NET actor 的互操作性 Yes DAPR1404 Dapr.Actors Usage Warning 1.16 调用 app.MapActorsHandlers 以映射 Dapr actor 的端点 Yes DAPR1501 Dapr.Jobs Usage Warning 1.16 Job 调用要求为 IEndpointRouteBuilder 上每个预期的作业设置和配置 MapDaprScheduledJobHandler No
分析器类别 以下是分析器可以分配到的各个有效类别,这些类别是按照 .NET 分析器使用的标准类别建模的:
Design Documentation Globalization Interoperability Maintainability Naming Performance Reliability Security Usage 1.10 - 使用 Dapr .NET SDK 开发应用程序 Dapr .NET SDK 的部署集成
同时考虑多个服务 使用你喜欢的 IDE 或编辑器启动应用程序时,通常假设你只需要运行一个东西:你正在调试的应用程序。然而,开发微服务会挑战你思考本地开发流程时同时考虑多个服务 。微服务应用程序包含多个你可能需要同时运行的服务,以及需要管理的依赖项(如状态存储)。
在开发流程中加入 Dapr 意味着你需要管理以下关注点:
你想要运行的每个服务 每个服务的 Dapr 边车 Dapr 组件和配置清单 额外的依赖项,如状态存储 可选:用于 Actor 的 Dapr Placement 服务 本文档假设你正在构建一个生产应用程序,并希望创建可重复且稳健的开发实践。此处提供的指导是通用的,适用于使用 Dapr 的任何 .NET 服务器应用程序(包括 Actor)。
管理组件 对于使用 Dapr 进行本地开发,你有两种主要方法来存储组件定义:
使用默认位置(~/.dapr/components) 使用你自己的位置 在源代码仓库中创建一个文件夹来存储组件和配置,将使你能够对这些定义进行版本控制和共享。此处提供的指导假设你在应用程序源代码旁边创建了一个文件夹来存储这些文件。
开发选项 选择以下链接之一,了解你可以在本地开发场景中使用的工具。建议你熟悉其中的每一个,以了解 .NET SDK 提供的选项。
1.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 命令。
💡 端口 由于您需要为每个服务配置唯一的端口,您可以使用此命令将该端口值传递给 Dapr 和服务两者 。--urls http://localhost:<port> 将配置 ASP.NET Core 在提供的端口上监听流量。在命令行中使用配置比在其他地方硬编码监听端口更灵活。如果您的任何服务不接受 HTTP 流量,则通过删除 --app-port 和 --urls 参数来修改上述命令。
后续步骤 如果您需要调试,请使用调试器的附加功能附加到正在运行的进程之一。
如果您想扩展这种方法,请考虑构建一个脚本,为整个应用程序自动执行此过程。
1.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 运行容器,一旦你的服务进入容器中,其配置与任何其他编程技术类似。
💡 应用端口 在容器中,ASP.NET Core 应用默认监听端口 80。稍后配置 --app-port 时请记住这一点。总结方法如下:
为每个服务创建一个 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 示例应用中查看更大的示例。
1.10.3 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发 了解使用 .NET Aspire 进行本地开发
.NET Aspire .NET Aspire 是一种开发工具,
旨在通过提供一个框架,使第三方服务能够与您自己的软件一起轻松集成、观察和配置,从而更轻松地将外部软件包含到 .NET 应用程序中。
Aspire 通过与流行的 IDE 提供丰富的集成来简化本地开发,包括
Microsoft Visual Studio 、
Visual Studio Code 、
JetBrains Rider 等,
以便在启动调试器启动应用程序的同时,自动启动和配置对其他集成的访问,包括 Dapr。
虽然 Aspire 还协助将应用程序部署到各种云主机(如 Microsoft Azure 和 Amazon AWS),但部署目前超出了本指南的范围。更多信息可以在 Aspire 的文档这里 找到。
可以在这里 找到一个端到端演示,其中包含以下内容并演示了多个启用 Dapr 的服务之间的服务调用。
前提条件 通过 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.csproj 和 BackEndApp\BackEndApp.csproj 中创建。
dotnet new web --name FrontEndApp
接下来,我们将配置 AppHost 项目以添加必要的包以支持本地 Dapr 开发。使用以下命令导航到 AppHost 目录,并将 CommunityToolkit.Aspire.Hosting.Dapr 包从 NuGet 安装到项目中。
我们还将添加对 FrontEndApp 项目的引用,以便我们可以在注册过程中引用它。
此包以前称为
Aspire.Hosting.Dapr,已被
标记为已弃用 。
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 );
如上例所示,从 .NET Aspire 9.0 开始,如果您打算使用 Dapr 需要调用您的应用程序的任何功能,例如发布订阅、actor 或工作流,您需要将 AppPort 指定为配置选项,因为 Aspire 不会在运行时自动将其传递给 Dapr。预计这种行为将在未来版本中发生变化,因为修复已合并,可以在
这里 进行跟踪。
最后,让我们向后端应用添加一个端点,我们可以使用 Dapr 的服务调用调用它以显示到页面以演示 Dapr 正在按预期工作。
当您在 IDE 中打开解决方案时,请确保 aspiredemo.AppHost 配置为您的启动项目,但是当您以调试配置启动它时,您会注意到您的集成控制台应该反映您预期的 Dapr 日志,并且它将对您的应用程序可用。
1.11 - 如何使用 Dapr .NET SDK 进行故障排除和调试 使用 Dapr .NET SDK 进行故障排除和调试的提示、技巧和指南
1.11.1 - 使用 .NET SDK 对发布订阅进行故障排查 使用 .NET SDK 对发布订阅进行故障排查
发布订阅故障排查 发布订阅最常见的问题是应用程序中的发布订阅端点未被调用。
这个问题有几个层次,对应不同的解决方案:
应用程序未接收到来自 Dapr 的任何流量 应用程序未向 Dapr 注册发布订阅端点 发布订阅端点已向 Dapr 注册,但请求未到达预期的端点 步骤 1:提高日志级别 这很重要。后续步骤将取决于你查看日志输出的能力。ASP.NET Core 在默认日志设置下几乎不输出任何内容,因此你需要更改它。
按照此处 的说明,调整日志详细程度以包含 ASP.NET Core 的 Information 级别日志。将 Microsoft 键设置为 Information。
步骤 2:验证你可以接收来自 Dapr 的流量 按正常方式启动应用程序(dapr run ...)。确保你在命令行中包含了 --app-port 参数。Dapr 需要知道你的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序在本地开发中会在端口 5000 上监听 HTTP。
等待 Dapr 完成启动
检查日志
你应该会看到类似以下的日志条目:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.....
在初始化期间,Dapr 会向你的应用程序发出一些配置请求。如果你找不到这些请求,说明出现了问题。请通过 issue 或 Discord 寻求帮助(并附上日志)。如果你看到向你的应用程序发出的请求,则继续执行步骤 3。
步骤 3:验证端点注册 按正常方式启动应用程序(dapr run ...)。
在命令行使用 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 输出。
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 (...)
在此示例中,Topic 和 HttpPost 特性是必需的,但其他细节可能不同。
如果你使用路由进行发布订阅,你应该有一个类似以下的端点:
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 信息来测试应用程序。
按正常方式启动应用程序(dapr run ...)。
在命令行使用 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 的内容。
2 - Dapr Go SDK 用于开发 Dapr 应用程序的 Go SDK 软件包
一个用于在 Go 中构建 Dapr 应用程序的客户端库。该客户端支持所有公共 Dapr API,同时注重地道的 Go 体验和开发者生产力。
2.1 - Dapr 客户端 Go SDK 入门 如何开始使用 Dapr Go SDK
Dapr 客户端包允许您从 Go 应用程序与其他 Dapr 应用程序进行交互。
前置条件 导入客户端包 import "github.com/dapr/go-sdk/client"
错误处理 Dapr 错误基于 gRPC 的丰富错误模型 。
以下代码展示了如何解析和处理错误详细信息的示例:
if err != nil {
st := status . Convert ( err )
fmt . Printf ( "Code: %s\n" , st . Code (). String ())
fmt . Printf ( "Message: %s\n" , st . Message ())
for _ , detail := range st . Details () {
switch t := detail .( type ) {
case * errdetails . ErrorInfo :
// 处理 ErrorInfo 详细信息
fmt . Printf ( "ErrorInfo:\n- Domain: %s\n- Reason: %s\n- Metadata: %v\n" , t . GetDomain (), t . GetReason (), t . GetMetadata ())
case * errdetails . BadRequest :
// 处理 BadRequest 详细信息
fmt . Println ( "BadRequest:" )
for _ , violation := range t . GetFieldViolations () {
fmt . Printf ( "- Key: %s\n" , violation . GetField ())
fmt . Printf ( "- The %q field was wrong: %s\n" , violation . GetField (), violation . GetDescription ())
}
case * errdetails . ResourceInfo :
// 处理 ResourceInfo 详细信息
fmt . Printf ( "ResourceInfo:\n- Resource type: %s\n- Resource name: %s\n- Owner: %s\n- Description: %s\n" ,
t . GetResourceType (), t . GetResourceName (), t . GetOwner (), t . GetDescription ())
case * errdetails . Help :
// 处理 ResourceInfo 详细信息
fmt . Println ( "HelpInfo:" )
for _ , link := range t . GetLinks () {
fmt . Printf ( "- Url: %s\n" , link . Url )
fmt . Printf ( "- Description: %s\n" , link . Description )
}
default :
// 为您期望的其他类型的详细信息添加 case
fmt . Printf ( "Unhandled error detail type: %v\n" , t )
}
}
}
构建块 Go SDK 允许您与所有 Dapr 构建块 进行交互。
服务调用 要在使用 Dapr 边车运行的另一个服务上调用特定方法,Dapr 客户端 Go SDK 提供了两个选项:
不使用数据调用服务:
resp , err := client . InvokeMethod ( ctx , "app-id" , "method-name" , "post" )
使用数据调用服务:
content := & dapr . DataContent {
ContentType : "application/json" ,
Data : [] byte ( `{ "id": "a123", "value": "demo", "valid": true }` ),
}
resp , err = client . InvokeMethodWithContent ( ctx , "app-id" , "method-name" , "post" , content )
有关服务调用的完整指南,请访问如何:调用服务 。
工作流 可以使用 Dapr Go SDK 编写和管理工作流及其活动,如下所示:
import (
...
"github.com/dapr/go-sdk/workflow"
...
func ExampleWorkflow ( ctx * workflow . WorkflowContext ) ( any , error ) {
var output string
input := "world"
if err := ctx . CallActivity ( ExampleActivity , workflow . ActivityInput ( input )). Await ( & output ); err != nil {
return nil , err
}
// 打印输出 - "hello world"
fmt . Println ( output )
return nil , nil
}
func ExampleActivity ( ctx workflow . ActivityContext ) ( any , error ) {
var input int
if err := ctx . GetInput ( & input ); err != nil {
return "" , err
}
return fmt . Sprintf ( "hello %s" , input ), nil
}
func main () {
// 创建工作流 worker
w , err := workflow . NewWorker ()
if err != nil {
log . Fatalf ( "error creating worker: %v" , err )
}
// 注册工作流
w . RegisterWorkflow ( ExampleWorkflow )
// 注册活动
w . RegisterActivity ( ExampleActivity )
// 启动工作流运行器
if err := w . Start (); err != nil {
log . Fatal ( err )
}
// 创建工作流客户端
wfClient , err := workflow . NewClient ()
if err != nil {
log . Fatal ( err )
}
// 启动新工作流
id , err := wfClient . ScheduleNewWorkflow ( context . Background (), "ExampleWorkflow" )
if err != nil {
log . Fatal ( err )
}
// 等待工作流完成
metadata , err := wfClient . WaitForWorkflowCompletion ( ctx , id )
if err != nil {
log . Fatal ( err )
}
// 打印完成后的工作流状态
fmt . Println ( metadata . RuntimeStatus )
// 关闭 Worker
w . Shutdown ()
}
有关更全面的工作流指南,请访问这些如何指南: 访问 Go SDK 示例以查看完整的示例: 状态管理 对于简单用例,Dapr 客户端提供了易于使用的 Save、Get、Delete 方法:
ctx := context . Background ()
data := [] byte ( "hello" )
store := "my-store" // 在组件 YAML 中定义
// 使用键 key1 保存状态,默认选项:强一致性、最后写入获胜
if err := client . SaveState ( ctx , store , "key1" , data , nil ); err != nil {
panic ( err )
}
// 获取键 key1 的状态
item , err := client . GetState ( ctx , store , "key1" , nil )
if err != nil {
panic ( err )
}
fmt . Printf ( "data [key:%s etag:%s]: %s" , item . Key , item . Etag , string ( item . Value ))
// 删除键 key1 的状态
if err := client . DeleteState ( ctx , store , "key1" , nil ); err != nil {
panic ( err )
}
对于更细粒度的控制,Dapr Go 客户端暴露了 SetStateItem 类型,可以用于获得对状态操作的更多控制,并允许一次保存多个项目:
item1 := & dapr . SetStateItem {
Key : "key1" ,
Etag : & ETag {
Value : "1" ,
},
Metadata : map [ string ] string {
"created-on" : time . Now (). UTC (). String (),
},
Value : [] byte ( "hello" ),
Options : & dapr . StateOptions {
Concurrency : dapr . StateConcurrencyLastWrite ,
Consistency : dapr . StateConsistencyStrong ,
},
}
item2 := & dapr . SetStateItem {
Key : "key2" ,
Metadata : map [ string ] string {
"created-on" : time . Now (). UTC (). String (),
},
Value : [] byte ( "hello again" ),
}
item3 := & dapr . SetStateItem {
Key : "key3" ,
Etag : & dapr . ETag {
Value : "1" ,
},
Value : [] byte ( "hello again" ),
}
if err := client . SaveBulkState ( ctx , store , item1 , item2 , item3 ); err != nil {
panic ( err )
}
类似地,GetBulkState 方法提供了在单个操作中检索多个状态项目的方法:
keys := [] string { "key1" , "key2" , "key3" }
items , err := client . GetBulkState ( ctx , store , keys , nil , 100 )
以及 ExecuteStateTransaction 方法以事务方式执行多个 upsert 或删除操作。
ops := make ([] * dapr . StateOperation , 0 )
op1 := & dapr . StateOperation {
Type : dapr . StateOperationTypeUpsert ,
Item : & dapr . SetStateItem {
Key : "key1" ,
Value : [] byte ( data ),
},
}
op2 := & dapr . StateOperation {
Type : dapr . StateOperationTypeDelete ,
Item : & dapr . SetStateItem {
Key : "key2" ,
},
}
ops = append ( ops , op1 , op2 )
meta := map [ string ] string {}
err := testClient . ExecuteStateTransaction ( ctx , store , meta , ops )
使用 QueryState 检索、过滤和排序存储在状态存储中的键/值数据。
// 定义查询字符串
query := `{
"filter": {
"EQ": { "value.Id": "1" }
},
"sort": [
{
"key": "value.Balance",
"order": "DESC"
}
]
}`
// 使用客户端查询状态
queryResponse , err := c . QueryState ( ctx , "querystore" , query )
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Got %d\n" , len ( queryResponse ))
for _ , account := range queryResponse {
var data Account
err := account . Unmarshal ( & data )
if err != nil {
log . Fatal ( err )
}
fmt . Printf ( "Account: %s has %f\n" , data . ID , data . Balance )
}
注意: 查询状态 API 目前处于 alpha 阶段
有关状态管理的完整指南,请访问如何:保存和获取状态 。
发布消息 要将数据发布到主题,Dapr Go 客户端提供了一个简单的方法:
data := [] byte ( `{ "id": "a123", "value": "abcdefg", "valid": true }` )
if err := client . PublishEvent ( ctx , "component-name" , "topic-name" , data ); err != nil {
panic ( err )
}
要一次发布多条消息,可以使用 PublishEvents 方法:
events := [] string { "event1" , "event2" , "event3" }
res := client . PublishEvents ( ctx , "component-name" , "topic-name" , events )
if res . Error != nil {
panic ( res . Error )
}
有关发布订阅的完整指南,请访问如何:发布和订阅 。
工作流 您可以使用 Go SDK 创建工作流 。例如,从一个简单的工作流活动开始:
func TestActivity ( ctx workflow . ActivityContext ) ( any , error ) {
var input int
if err := ctx . GetInput ( & input ); err != nil {
return "" , err
}
// 在这里做些事情
return "result" , nil
}
编写一个简单的工作流函数:
func TestWorkflow ( ctx * workflow . WorkflowContext ) ( any , error ) {
var input int
if err := ctx . GetInput ( & input ); err != nil {
return nil , err
}
var output string
if err := ctx . CallActivity ( TestActivity , workflow . ActivityInput ( input )). Await ( & output ); err != nil {
return nil , err
}
if err := ctx . WaitForExternalEvent ( "testEvent" , time . Second * 60 ). Await ( & output ); err != nil {
return nil , err
}
if err := ctx . CreateTimer ( time . Second ). Await ( nil ); err != nil {
return nil , nil
}
return output , nil
}
然后组合您的应用程序,使用您创建的工作流。请参阅如何:编写工作流指南 以获取完整的演练。
尝试 Go SDK 工作流示例 。
作业 Dapr 客户端 Go SDK 允许您调度、获取和删除作业。作业使您能够安排在特定时间或间隔执行工作。
调度作业 要调度新作业,使用 ScheduleJobAlpha1 方法:
import (
"google.golang.org/protobuf/types/known/anypb"
)
// 创建作业数据
data , err := anypb . New ( & YourDataStruct { Message : "Hello, Job!" })
if err != nil {
panic ( err )
}
// 使用构建器模式创建简单作业
job := client . NewJob ( "my-scheduled-job" ,
client . WithJobData ( data ),
client . WithJobDueTime ( "10s" ), // 10 秒后执行
)
// 调度作业
err = client . ScheduleJobAlpha1 ( ctx , job )
if err != nil {
panic ( err )
}
带调度和重复的作业 您可以使用带有 cron 表达式的 Schedule 字段创建重复作业:
job := client . NewJob ( "recurring-job" ,
client . WithJobData ( data ),
client . WithJobSchedule ( "0 9 * * *" ), // 每天上午 9 点运行
client . WithJobRepeats ( 10 ), // 重复 10 次
client . WithJobTTL ( "1h" ), // 作业在 1 小时后过期
)
err = client . ScheduleJobAlpha1 ( ctx , job )
带失败策略的作业 使用失败策略配置作业应如何处理失败:
// 带最大重试次数和间隔的常量重试策略
job := client . NewJob ( "resilient-job" ,
client . WithJobData ( data ),
client . WithJobDueTime ( "2024-01-01T10:00:00Z" ),
client . WithJobConstantFailurePolicy (),
client . WithJobConstantFailurePolicyMaxRetries ( 3 ),
client . WithJobConstantFailurePolicyInterval ( 30 * time . Second ),
)
err = client . ScheduleJobAlpha1 ( ctx , job )
对于失败时不应重试的作业,使用 drop 策略:
job := client . NewJob ( "one-shot-job" ,
client . WithJobData ( data ),
client . WithJobDueTime ( "2024-01-01T10:00:00Z" ),
client . WithJobDropFailurePolicy (),
)
err = client . ScheduleJobAlpha1 ( ctx , job )
获取作业 要获取有关调度作业的信息:
job , err := client . GetJobAlpha1 ( ctx , "my-scheduled-job" )
if err != nil {
panic ( err )
}
fmt . Printf ( "Job: %s, Schedule: %s, Repeats: %d\n" ,
job . Name , job . Schedule , job . Repeats )
删除作业 要取消调度作业:
err = client . DeleteJobAlpha1 ( ctx , "my-scheduled-job" )
if err != nil {
panic ( err )
}
有关作业的完整指南,请访问如何:调度和管理作业 。
输出绑定 Dapr Go 客户端 SDK 提供了两种方法来调用 Dapr 定义的绑定上的操作。Dapr 支持输入、输出和双向绑定。
对于简单的仅输出绑定:
in := & dapr . InvokeBindingRequest { Name : "binding-name" , Operation : "operation-name" }
err = client . InvokeOutputBinding ( ctx , in )
要使用内容和元数据调用方法:
in := & dapr . InvokeBindingRequest {
Name : "binding-name" ,
Operation : "operation-name" ,
Data : [] byte ( "hello" ),
Metadata : map [ string ] string { "k1" : "v1" , "k2" : "v2" },
}
out , err := client . InvokeBinding ( ctx , in )
有关输出绑定的完整指南,请访问如何:使用绑定 。
Actors 使用 Dapr Go 客户端 SDK 编写 actors。
// MyActor 表示一个示例 actor 类型。
type MyActor struct {
actors . Actor
}
// MyActorMethod 是可以在 MyActor 上调用的方法。
func ( a * MyActor ) MyActorMethod ( ctx context . Context , req * actors . Message ) ( string , error ) {
log . Printf ( "Received message: %s" , req . Data )
return "Hello from MyActor!" , nil
}
func main () {
// 创建 Dapr 客户端
daprClient , err := client . NewClient ()
if err != nil {
log . Fatal ( "Error creating Dapr client: " , err )
}
// 使用 Dapr 注册 actor 类型
actors . RegisterActor ( & MyActor {})
// 创建 actor 客户端
actorClient := actors . NewClient ( daprClient )
// 创建 actor ID
actorID := actors . NewActorID ( "myactor" )
// 获取或创建 actor
err = actorClient . SaveActorState ( context . Background (), "myactorstore" , actorID , map [ string ] interface {}{ "data" : "initial state" })
if err != nil {
log . Fatal ( "Error saving actor state: " , err )
}
// 在 actor 上调用方法
resp , err := actorClient . InvokeActorMethod ( context . Background (), "myactorstore" , actorID , "MyActorMethod" , & actors . Message { Data : [] byte ( "Hello from client!" )})
if err != nil {
log . Fatal ( "Error invoking actor method: " , err )
}
log . Printf ( "Response from actor: %s" , resp . Data )
// 在终止之前等待几秒钟
time . Sleep ( 5 * time . Second )
// 删除 actor
err = actorClient . DeleteActor ( context . Background (), "myactorstore" , actorID )
if err != nil {
log . Fatal ( "Error deleting actor: " , err )
}
// 关闭 Dapr 客户端
daprClient . Close ()
}
有关 actors 的完整指南,请访问 Actors 构建块文档 。
密钥管理 Dapr 客户端还提供对运行时密钥的访问,这些密钥可以由任意数量的密钥存储支持(例如 Kubernetes Secrets、HashiCorp Vault 或 Azure KeyVault):
opt := map [ string ] string {
"version" : "2" ,
}
secret , err := client . GetSecret ( ctx , "store-name" , "secret-name" , opt )
身份验证 默认情况下,Dapr 依赖网络边界来限制对其 API 的访问。但是,如果目标 Dapr API 配置了基于令牌的身份验证,用户可以通过两种方式使用该令牌配置 Go Dapr 客户端:
环境变量
如果定义了 DAPR_API_TOKEN 环境变量,Dapr 将自动使用它来增强其 Dapr API 调用以确保身份验证。
显式方法
此外,用户还可以在任何 Dapr 客户端实例上显式设置 API 令牌。当用户代码需要为不同的 Dapr API 端点创建多个客户端时,此方法很有用。
func main () {
client , err := dapr . NewClient ()
if err != nil {
panic ( err )
}
defer client . Close ()
client . WithAuthToken ( "your-Dapr-API-token-here" )
}
有关密钥的完整指南,请访问如何:检索密钥 。
分布式锁 Dapr 客户端使用锁提供对资源的互斥访问。使用锁,您可以:
提供对数据库行、表或整个数据库的访问 按顺序锁定从队列读取消息 package main
import (
"fmt"
dapr "github.com/dapr/go-sdk/client"
)
func main () {
client , err := dapr . NewClient ()
if err != nil {
panic ( err )
}
defer client . Close ()
resp , err := client . TryLockAlpha1 ( ctx , "lockstore" , & dapr . LockRequest {
LockOwner : "random_id_abc123" ,
ResourceID : "my_file_name" ,
ExpiryInSeconds : 60 ,
})
fmt . Println ( resp . Success )
}
有关分布式锁的完整指南,请访问如何:使用锁 。
配置 使用 Dapr 客户端 Go SDK,您可以消费作为只读键/值对返回的配置项,并订阅配置项更改。
配置获取 items , err := client . GetConfigurationItem ( ctx , "example-config" , "mykey" )
if err != nil {
panic ( err )
}
fmt . Printf ( "get config = %s\n" , ( * items ). Value )
配置订阅 go func () {
if err := client . SubscribeConfigurationItems ( ctx , "example-config" , [] string { "mySubscribeKey1" , "mySubscribeKey2" , "mySubscribeKey3" }, func ( id string , items map [ string ] * dapr . ConfigurationItem ) {
for k , v := range items {
fmt . Printf ( "get updated config key = %s, value = %s \n" , k , v . Value )
}
subscribeID = id
}); err != nil {
panic ( err )
}
}()
有关配置的完整指南,请访问如何:从存储管理配置 。
密码学 使用 Dapr 客户端 Go SDK,您可以使用高级别 Encrypt 和 Decrypt 密码学 API 在处理数据流时加密和解密文件。
要加密:
// 使用 Dapr 加密数据
out , err := client . Encrypt ( context . Background (), rf , dapr . EncryptOptions {
// 这是 3 个必需参数
ComponentName : "mycryptocomponent" ,
KeyName : "mykey" ,
Algorithm : "RSA" ,
})
if err != nil {
panic ( err )
}
要解密:
// 使用 Dapr 解密数据
out , err := client . Decrypt ( context . Background (), rf , dapr . EncryptOptions {
// 唯一必需的选项是组件名称
ComponentName : "mycryptocomponent" ,
})
有关密码学的完整指南,请访问如何:使用密码学 API 。
相关链接 Go SDK 示例
2.2 - Dapr Service(回调)SDK for Go 入门 如何快速上手 Dapr Service(回调)SDK for Go
除了 Dapr API 客户端之外,Dapr Go SDK 还提供了 service 包,用于引导启动你的 Dapr 回调服务。这些服务可以基于 gRPC 或 HTTP 进行开发:
2.2.1 - Dapr HTTP Service SDK for Go 入门 如何快速上手使用 Dapr HTTP Service SDK for Go
前置条件 首先导入 Dapr Go service/http 包:
daprd "github.com/dapr/go-sdk/service/http"
创建并启动服务 要创建一个 HTTP Dapr 服务,首先需要使用特定地址创建一个 Dapr 回调实例:
s := daprd . NewService ( ":8080" )
或者使用地址和现有的 http.ServeMux,以便合并现有的服务器实现:
mux := http . NewServeMux ()
mux . HandleFunc ( "/" , myOtherHandler )
s := daprd . NewServiceWithMux ( ":8080" , mux )
创建服务实例后,您可以为该服务"附加"任意数量的事件、绑定和服务调用逻辑处理器,如下所示。一旦定义了逻辑,就可以启动服务了:
if err := s . Start (); err != nil && err != http . ErrServerClosed {
log . Fatalf ( "error: %v" , err )
}
事件处理 要处理来自特定主题的事件,需要在启动服务之前添加至少一个主题事件处理器:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/events" ,
}
err := s . AddTopicEventHandler ( sub , eventHandler )
if err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
处理器方法本身可以是任何符合预期签名的方法:
func eventHandler ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// do something with the event
return true , nil
}
或者,您可以使用路由规则 根据 CloudEvent 的内容将消息发送到不同的处理器。
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/important" ,
Match : `event.type == "important"` ,
Priority : 1 ,
}
err := s . AddTopicEventHandler ( sub , importantHandler )
if err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
您也可以创建一个实现了 TopicEventSubscriber 接口的自定义类型来处理事件:
type EventHandler struct {
// any data or references that your event handler needs.
}
func ( h * EventHandler ) Handle ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// do something with the event
return true , nil
}
然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
eventHandler := & EventHandler {
// initialize any fields
}
if err := s . AddTopicEventSubscriber ( sub , eventHandler ); err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
服务调用处理器 要处理服务调用,需要在启动服务之前添加至少一个服务调用处理器:
if err := s . AddServiceInvocationHandler ( "/echo" , echoHandler ); err != nil {
log . Fatalf ( "error adding invocation handler: %v" , err )
}
处理器方法本身可以是任何符合预期签名的方法:
func echoHandler ( ctx context . Context , in * common . InvocationEvent ) ( out * common . Content , err error ) {
log . Printf ( "echo - ContentType:%s, Verb:%s, QueryString:%s, %+v" , in . ContentType , in . Verb , in . QueryString , string ( in . Data ))
// do something with the invocation here
out = & common . Content {
Data : in . Data ,
ContentType : in . ContentType ,
DataTypeURL : in . DataTypeURL ,
}
return
}
绑定调用处理器 if err := s . AddBindingInvocationHandler ( "/run" , runHandler ); err != nil {
log . Fatalf ( "error adding binding handler: %v" , err )
}
处理器方法本身可以是任何符合预期签名的方法:
func runHandler ( ctx context . Context , in * common . BindingEvent ) ( out [] byte , err error ) {
log . Printf ( "binding - Data:%v, Meta:%v" , in . Data , in . Metadata )
// do something with the invocation here
return nil , nil
}
相关链接 2.2.2 - Dapr 服务(回调)SDK for Go 入门 如何快速上手 Dapr Go 服务(回调)SDK
Dapr gRPC 服务 SDK for Go 前置条件 首先导入 Dapr Go service/grpc 包:
daprd "github.com/dapr/go-sdk/service/grpc"
创建和启动服务 要创建一个 gRPC Dapr 服务,首先,使用特定地址创建一个 Dapr 回调实例:
s , err := daprd . NewService ( ":50001" )
if err != nil {
log . Fatalf ( "failed to start the server: %v" , err )
}
或者使用地址和一个已有的 net.Listener,以便与现有的服务监听器结合:
list , err := net . Listen ( "tcp" , "localhost:0" )
if err != nil {
log . Fatalf ( "gRPC listener creation failed: %s" , err )
}
s := daprd . NewServiceWithListener ( list )
一旦创建了服务实例,您就可以为该服务"附加"任意数量的事件、绑定和服务调用逻辑处理器,如下所示。逻辑定义完成后,就可以启动服务了:
if err := s . Start (); err != nil {
log . Fatalf ( "server error: %v" , err )
}
事件处理 要处理来自特定主题的事件,您需要在启动服务之前添加至少一个主题事件处理器:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
if err := s . AddTopicEventHandler ( sub , eventHandler ); err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
处理器方法本身可以是任何具有预期签名的方法:
func eventHandler ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// do something with the event
return true , nil
}
或者,您可以使用路由规则 ,根据 CloudEvent 的内容将消息发送到不同的处理器。
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
Route : "/important" ,
Match : `event.type == "important"` ,
Priority : 1 ,
}
err := s . AddTopicEventHandler ( sub , importantHandler )
if err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
您还可以创建一个实现 TopicEventSubscriber 接口的自定义类型来处理您的事件:
type EventHandler struct {
// any data or references that your event handler needs.
}
func ( h * EventHandler ) Handle ( ctx context . Context , e * common . TopicEvent ) ( retry bool , err error ) {
log . Printf ( "event - PubsubName:%s, Topic:%s, ID:%s, Data: %v" , e . PubsubName , e . Topic , e . ID , e . Data )
// do something with the event
return true , nil
}
然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler:
sub := & common . Subscription {
PubsubName : "messages" ,
Topic : "topic1" ,
}
eventHandler := & EventHandler {
// initialize any fields
}
if err := s . AddTopicEventSubscriber ( sub , eventHandler ); err != nil {
log . Fatalf ( "error adding topic subscription: %v" , err )
}
服务调用处理器 要处理服务调用,您需要在启动服务之前添加至少一个服务调用处理器:
if err := s . AddServiceInvocationHandler ( "echo" , echoHandler ); err != nil {
log . Fatalf ( "error adding invocation handler: %v" , err )
}
处理器方法本身可以是任何具有预期签名的方法:
func echoHandler ( ctx context . Context , in * common . InvocationEvent ) ( out * common . Content , err error ) {
log . Printf ( "echo - ContentType:%s, Verb:%s, QueryString:%s, %+v" , in . ContentType , in . Verb , in . QueryString , string ( in . Data ))
// do something with the invocation here
out = & common . Content {
Data : in . Data ,
ContentType : in . ContentType ,
DataTypeURL : in . DataTypeURL ,
}
return
}
绑定调用处理器 要处理绑定调用,您需要在启动服务之前添加至少一个绑定调用处理器:
if err := s . AddBindingInvocationHandler ( "run" , runHandler ); err != nil {
log . Fatalf ( "error adding binding handler: %v" , err )
}
处理器方法本身可以是任何具有预期签名的方法:
func runHandler ( ctx context . Context , in * common . BindingEvent ) ( out [] byte , err error ) {
log . Printf ( "binding - Data:%v, Meta:%v" , in . Data , in . Metadata )
// do something with the invocation here
return nil , nil
}
相关链接 3 - Dapr Java SDK 用于开发 Dapr 应用程序的 Java SDK 软件包
Dapr 提供了多种软件包来协助 Java 应用程序的开发。使用它们,你可以通过 Dapr 创建 Java 客户端、服务器和虚拟 Actor。
前提条件 已安装 Dapr CLI 已初始化 Dapr 环境 JDK 11 或更高版本 - 已发布的 jar 文件与 Java 8 兼容: 安装以下任一 Java 构建工具: 导入 Dapr Java SDK 接下来,导入 Java SDK 软件包以开始使用。选择你首选的构建工具以了解如何导入。
对于 Maven 项目,将以下内容添加到你的 pom.xml 文件中:
<project>
...
<dependencies>
...
<!-- Dapr's core SDK with all features, except Actors. -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk</artifactId>
<version> 1.16.0</version>
</dependency>
<!-- Dapr's SDK for Actors (optional). -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk-actors</artifactId>
<version> 1.16.0</version>
</dependency>
<!-- Dapr's SDK integration with SpringBoot (optional). -->
<dependency>
<groupId> io.dapr</groupId>
<artifactId> dapr-sdk-springboot</artifactId>
<version> 1.16.0</version>
</dependency>
...
</dependencies>
...
</project>
对于 Gradle 项目,将以下内容添加到你的 build.gradle 文件中:
dependencies {
...
// Dapr's core SDK with all features, except Actors.
compile ( ' io . dapr : dapr - sdk : 1 . 16 . 0 ' )
// Dapr's SDK for Actors (optional).
compile ( ' io . dapr : dapr - sdk - actors : 1 . 16 . 0 ' )
// Dapr's SDK integration with SpringBoot (optional).
compile ( ' io . dapr : dapr - sdk - springboot : 1 . 16 . 0 ' )
}
如果你还使用了 Spring Boot,可能会遇到一个常见问题:Dapr SDK 使用的 OkHttp 版本与 Spring Boot 的 Bill of Materials 中指定的版本冲突。
你可以通过在项目中指定与 Dapr SDK 使用的版本兼容的 OkHttp 版本来解决此问题:
<dependency>
<groupId> com.squareup.okhttp3</groupId>
<artifactId> okhttp</artifactId>
<version> 1.16.0</version>
</dependency>
试用 测试 Dapr Java SDK。通过 Java 快速入门和教程来查看 Dapr 的实际效果:
SDK 示例 描述 快速入门 在几分钟内使用 Java SDK 体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始使用。
import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// sending a class with message; BINDING_OPERATION="create"
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , myClass ). block ();
// sending a plain string
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , message ). block ();
}
可用软件包 客户端 创建与 Dapr 边车和其他 Dapr 应用程序交互的 Java 客户端。
工作流 在 Java 中创建和管理与其他 Dapr API 协同工作的 workflow。
3.1 - AI 借助 Dapr Conversation AI 包,您可以从 Java 应用程序与 Dapr AI 工作负载进行交互。要开始使用,请参阅
Dapr AI 操作指南。
3.1.1 - 操作指南:使用 Java SDK 编写和管理 Dapr Conversation AI 如何使用 Dapr Java SDK 快速上手 Conversation AI
在本次演示中,我们将介绍如何使用 Conversation API 与大语言模型(LLM)进行对话。该 API 会返回给定提示词的 LLM 响应。通过提供的 conversation ai 示例 ,你将:
此示例使用自托管模式 下 dapr init 的默认配置。
前置条件 设置环境 克隆 Java SDK 仓库 并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行 Conversation AI 示例所需的依赖。
mvn clean install -DskipTests
从 Java SDK 根目录进入示例目录。
运行 Dapr 边车。
dapr run --app-id conversationapp --dapr-grpc-port 51439 --dapr-http-port 3500 --app-port 8080
现在,Dapr 正在 http://localhost:3500 监听 HTTP 请求,在 http://localhost:51439 监听 gRPC 请求。
向 Conversation AI API 发送包含个人身份信息(PII)的提示词 在 DemoConversationAI 中,包含使用 DaprPreviewClient 下的 converse 方法发送提示词的步骤。
public class DemoConversationAI {
/**
* 启动客户端的 main 方法。
*
* @param args 输入参数(未使用)。
*/
public static void main ( String [] args ) {
try ( DaprPreviewClient client = new DaprClientBuilder (). buildPreviewClient ()) {
System . out . println ( "Sending the following input to LLM: Hello How are you? This is the my number 672-123-4567" );
ConversationInput daprConversationInput = new ConversationInput ( "Hello How are you? "
+ "This is the my number 672-123-4567" );
// 组件名称是在 conversation.yaml 文件的 metadata 块中提供的名称。
Mono < ConversationResponse > responseMono = client . converse ( new ConversationRequest ( "echo" ,
List . of ( daprConversationInput ))
. setContextId ( "contextId" )
. setScrubPii ( true ). setTemperature ( 1 . 1d ));
ConversationResponse response = responseMono . block ();
System . out . printf ( "Conversation output: %s" , response . getConversationOutputs (). get ( 0 ). getResult ());
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
}
}
使用以下命令运行 DemoConversationAI。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.conversation.DemoConversationAI
示例输出 == APP == Conversation output: Hello How are you? This is the my number <ISBN>
如输出所示,发送到 API 的号码已被混淆并以 <ISBN> 的形式返回。
上面的示例使用了一个“echo”
组件进行测试,该组件只是简单地返回输入消息。
当与 OpenAI 或 Claude 等 LLM 集成时,你将收到有意义的响应,而不是回显的输入。
后续步骤 3.2 - Dapr 客户端 Java SDK 入门 如何开始使用 Dapr Java SDK
Dapr 客户端包允许您从 Java 应用程序与其他 Dapr 应用程序进行交互。
注意 如果您还没有,请
尝试其中一个快速入门 ,快速了解如何将 Dapr Java SDK 与 API 构建块一起使用。
前置条件 完成初始设置并将 Java SDK 导入您的项目
初始化客户端 您可以像这样初始化 Dapr 客户端:
DaprClient client = new DaprClientBuilder (). build ()
这将连接到默认的 Dapr gRPC 端点 localhost:50001。有关使用环境变量和系统属性配置客户端的信息,请参阅属性 。
错误处理 最初,Dapr 中的错误遵循标准 gRPC 错误模型。然而,为了提供更详细和有信息量的错误消息,在 1.13 版本中引入了增强的错误模型,该模型与 gRPC 更丰富的错误模型保持一致。作为响应,Java SDK 扩展了 DaprException 以包含 Dapr 中添加的错误详细信息。
使用 Dapr Java SDK 时处理 DaprException 并使用错误详细信息的示例:
...
try {
client . publishEvent ( "unknown_pubsub" , "mytopic" , "mydata" ). block ();
} catch ( DaprException exception ) {
System . out . println ( "Dapr exception's error code: " + exception . getErrorCode ());
System . out . println ( "Dapr exception's message: " + exception . getMessage ());
// DaprException 现在包含 `getStatusDetails()` 以包含有关 Dapr 运行时错误的更多详细信息。
System . out . println ( "Dapr exception's reason: " + exception . getStatusDetails (). get (
DaprErrorDetails . ErrorDetailType . ERROR_INFO ,
"reason" ,
TypeRef . STRING ));
}
...
构建块 Java SDK 允许您与所有 Dapr 构建块 进行交互。
调用服务 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 调用 'GET' 方法 (HTTP),跳过序列化:使用 Mono<byte[]> 返回类型
// 对于 gRPC,请在下面设置 HttpExtension.NONE 参数
response = client . invokeMethod ( SERVICE_TO_INVOKE , METHOD_TO_INVOKE , "{\"name\":\"World!\"}" , HttpExtension . GET , byte [] . class ). block ();
// 调用 'POST' 方法 (HTTP),跳过序列化:使用 Mono<byte[]> 返回类型
response = client . invokeMethod ( SERVICE_TO_INVOKE , METHOD_TO_INVOKE , "{\"id\":\"100\", \"FirstName\":\"Value\", \"LastName\":\"Value\"}" , HttpExtension . POST , byte [] . class ). block ();
System . out . println ( new String ( response ));
// 调用 'POST' 方法 (HTTP),使用序列化:使用 Mono<Employee> 返回类型
Employee newEmployee = new Employee ( "Nigel" , "Guitarist" );
Employee employeeResponse = client . invokeMethod ( SERVICE_TO_INVOKE , "employees" , newEmployee , HttpExtension . POST , Employee . class ). block ();
}
保存和获取应用程序状态 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.domain.State ;
import reactor.core.publisher.Mono ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 保存状态
client . saveState ( STATE_STORE_NAME , FIRST_KEY_NAME , myClass ). block ();
// 获取状态
State < MyClass > retrievedMessage = client . getState ( STATE_STORE_NAME , FIRST_KEY_NAME , MyClass . class ). block ();
// 删除状态
client . deleteState ( STATE_STORE_NAME , FIRST_KEY_NAME ). block ();
}
发布和订阅消息 发布消息 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.domain.Metadata ;
import static java.util.Collections.singletonMap ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
client . publishEvent ( PUBSUB_NAME , TOPIC_NAME , message , singletonMap ( Metadata . TTL_IN_SECONDS , MESSAGE_TTL_IN_SECONDS )). block ();
}
订阅消息 import com.fasterxml.jackson.databind.ObjectMapper ;
import io.dapr.Topic ;
import io.dapr.client.domain.BulkSubscribeAppResponse ;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry ;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus ;
import io.dapr.client.domain.BulkSubscribeMessage ;
import io.dapr.client.domain.BulkSubscribeMessageEntry ;
import io.dapr.client.domain.CloudEvent ;
import io.dapr.springboot.annotations.BulkSubscribe ;
import org.springframework.web.bind.annotation.PostMapping ;
import org.springframework.web.bind.annotation.RequestBody ;
import org.springframework.web.bind.annotation.RestController ;
import reactor.core.publisher.Mono ;
@RestController
public class SubscriberController {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
@Topic ( name = "testingtopic" , pubsubName = "${myAppProperty:messagebus}" )
@PostMapping ( path = "/testingtopic" )
public Mono < Void > handleMessage ( @RequestBody ( required = false ) CloudEvent <?> cloudEvent ) {
return Mono . fromRunnable (() -> {
try {
System . out . println ( "Subscriber got: " + cloudEvent . getData ());
System . out . println ( "Subscriber got: " + OBJECT_MAPPER . writeValueAsString ( cloudEvent ));
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
});
}
@Topic ( name = "testingtopic" , pubsubName = "${myAppProperty:messagebus}" ,
rule = @Rule ( match = "event.type == 'myevent.v2'" , priority = 1 ))
@PostMapping ( path = "/testingtopicV2" )
public Mono < Void > handleMessageV2 ( @RequestBody ( required = false ) CloudEvent envelope ) {
return Mono . fromRunnable (() -> {
try {
System . out . println ( "Subscriber got: " + cloudEvent . getData ());
System . out . println ( "Subscriber got: " + OBJECT_MAPPER . writeValueAsString ( cloudEvent ));
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
});
}
@BulkSubscribe ()
@Topic ( name = "testingtopicbulk" , pubsubName = "${myAppProperty:messagebus}" )
@PostMapping ( path = "/testingtopicbulk" )
public Mono < BulkSubscribeAppResponse > handleBulkMessage (
@RequestBody ( required = false ) BulkSubscribeMessage < CloudEvent < String >> bulkMessage ) {
return Mono . fromCallable (() -> {
if ( bulkMessage . getEntries (). size () == 0 ) {
return new BulkSubscribeAppResponse ( new ArrayList < BulkSubscribeAppResponseEntry > ());
}
System . out . println ( "Bulk Subscriber received " + bulkMessage . getEntries (). size () + " messages." );
List < BulkSubscribeAppResponseEntry > entries = new ArrayList < BulkSubscribeAppResponseEntry > ();
for ( BulkSubscribeMessageEntry <?> entry : bulkMessage . getEntries ()) {
try {
System . out . printf ( "Bulk Subscriber message has entry ID: %s\n" , entry . getEntryId ());
CloudEvent <?> cloudEvent = ( CloudEvent <?> ) entry . getEvent ();
System . out . printf ( "Bulk Subscriber got: %s\n" , cloudEvent . getData ());
entries . add ( new BulkSubscribeAppResponseEntry ( entry . getEntryId (), BulkSubscribeAppResponseStatus . SUCCESS ));
} catch ( Exception e ) {
e . printStackTrace ();
entries . add ( new BulkSubscribeAppResponseEntry ( entry . getEntryId (), BulkSubscribeAppResponseStatus . RETRY ));
}
}
return new BulkSubscribeAppResponse ( entries );
});
}
}
批量发布消息 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.domain.BulkPublishResponse ;
import io.dapr.client.domain.BulkPublishResponseFailedEntry ;
import java.util.ArrayList ;
import java.util.List ;
class Solution {
public void publishMessages () {
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 创建要发布的消息列表
List < String > messages = new ArrayList <> ();
for ( int i = 0 ; i < NUM_MESSAGES ; i ++ ) {
String message = String . format ( "This is message #%d" , i );
messages . add ( message );
System . out . println ( "Going to publish message : " + message );
}
// 使用批量发布 API 发布消息列表
BulkPublishResponse < String > res = client . publishEvents ( PUBSUB_NAME , TOPIC_NAME , "text/plain" , messages ). block ()
}
}
}
与输出绑定交互 import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
// 使用消息发送类;BINDING_OPERATION="create"
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , myClass ). block ();
// 发送纯字符串
client . invokeBinding ( BINDING_NAME , BINDING_OPERATION , message ). block ();
}
与输入绑定交互 import org.springframework.web.bind.annotation.* ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
@RestController
@RequestMapping ( "/" )
public class myClass {
private static final Logger log = LoggerFactory . getLogger ( myClass );
@PostMapping ( path = "/checkout" )
public Mono < String > getCheckout ( @RequestBody ( required = false ) byte [] body ) {
return Mono . fromRunnable (() ->
log . info ( "Received Message: " + new String ( body )));
}
}
获取密钥 import com.fasterxml.jackson.databind.ObjectMapper ;
import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import java.util.Map ;
try ( DaprClient client = ( new DaprClientBuilder ()). build ()) {
Map < String , String > secret = client . getSecret ( SECRET_STORE_NAME , secretKey ). block ();
System . out . println ( JSON_SERIALIZER . writeValueAsString ( secret ));
}
Actor Actor 是一个隔离的、独立的计算和状态单元,具有单线程执行。Dapr 提供基于虚拟 Actor 模式 的 actor 实现,该模式提供单线程编程模型,其中 actor 在不使用时会被垃圾回收。使用 Dapr 的实现,您可以根据 Actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性。
import io.dapr.actors.ActorMethod ;
import io.dapr.actors.ActorType ;
import reactor.core.publisher.Mono ;
@ActorType ( name = "DemoActor" )
public interface DemoActor {
void registerReminder ();
@ActorMethod ( name = "echo_message" )
String say ( String something );
void clock ( String message );
@ActorMethod ( returns = Integer . class )
Mono < Integer > incrementAndGet ( int delta );
}
获取和订阅应用程序配置 请注意,这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.ConfigurationItem ;
import io.dapr.client.domain.GetConfigurationRequest ;
import io.dapr.client.domain.SubscribeConfigurationRequest ;
import reactor.core.publisher.Flux ;
import reactor.core.publisher.Mono ;
try ( DaprPreviewClient client = ( new DaprClientBuilder ()). buildPreviewClient ()) {
// 获取单个键的配置
Mono < ConfigurationItem > item = client . getConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY ). block ();
// 获取多个键的配置
Mono < Map < String , ConfigurationItem >> items =
client . getConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY_1 , CONFIG_KEY_2 );
// 订阅配置更改
Flux < SubscribeConfigurationResponse > outFlux = client . subscribeConfiguration ( CONFIG_STORE_NAME , CONFIG_KEY_1 , CONFIG_KEY_2 );
outFlux . subscribe ( configItems -> configItems . forEach (...));
// 取消订阅配置更改
Mono < UnsubscribeConfigurationResponse > unsubscribe = client . unsubscribeConfiguration ( SUBSCRIPTION_ID , CONFIG_STORE_NAME )
}
查询已保存的状态 请注意,这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问
import io.dapr.client.DaprClient ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.QueryStateItem ;
import io.dapr.client.domain.QueryStateRequest ;
import io.dapr.client.domain.QueryStateResponse ;
import io.dapr.client.domain.query.Query ;
import io.dapr.client.domain.query.Sorting ;
import io.dapr.client.domain.query.filters.EqFilter ;
try ( DaprClient client = builder . build (); DaprPreviewClient previewClient = builder . buildPreviewClient ()) {
String searchVal = args . length == 0 ? "searchValue" : args [ 0 ] ;
// 创建 JSON 数据
Listing first = new Listing ();
first . setPropertyType ( "apartment" );
first . setId ( "1000" );
...
Listing second = new Listing ();
second . setPropertyType ( "row-house" );
second . setId ( "1002" );
...
Listing third = new Listing ();
third . setPropertyType ( "apartment" );
third . setId ( "1003" );
...
Listing fourth = new Listing ();
fourth . setPropertyType ( "apartment" );
fourth . setId ( "1001" );
...
Map < String , String > meta = new HashMap <> ();
meta . put ( "contentType" , "application/json" );
// 保存状态
SaveStateRequest request = new SaveStateRequest ( STATE_STORE_NAME ). setStates (
new State <> ( "1" , first , null , meta , null ),
new State <> ( "2" , second , null , meta , null ),
new State <> ( "3" , third , null , meta , null ),
new State <> ( "4" , fourth , null , meta , null )
);
client . saveBulkState ( request ). block ();
// 创建查询和查询状态请求
Query query = new Query ()
. setFilter ( new EqFilter <> ( "propertyType" , "apartment" ))
. setSort ( Arrays . asList ( new Sorting ( "id" , Sorting . Order . DESC )));
QueryStateRequest request = new QueryStateRequest ( STATE_STORE_NAME )
. setQuery ( query );
// 使用预览客户端调用查询状态 API
QueryStateResponse < MyData > result = previewClient . queryState ( request , MyData . class ). block ();
// 查看查询状态响应
System . out . println ( "Found " + result . getResults (). size () + " items." );
for ( QueryStateItem < Listing > item : result . getResults ()) {
System . out . println ( "Key: " + item . getKey ());
System . out . println ( "Data: " + item . getValue ());
}
}
分布式锁 package io.dapr.examples.lock.grpc ;
import io.dapr.client.DaprClientBuilder ;
import io.dapr.client.DaprPreviewClient ;
import io.dapr.client.domain.LockRequest ;
import io.dapr.client.domain.UnlockRequest ;
import io.dapr.client.domain.UnlockResponseStatus ;
import reactor.core.publisher.Mono ;
public class DistributedLockGrpcClient {
private static final String LOCK_STORE_NAME = "lockstore" ;
/**
* 执行各种方法以检查不同的 API。
*
* @param args 参数
* @throws Exception 抛出异常
*/
public static void main ( String [] args ) throws Exception {
try ( DaprPreviewClient client = ( new DaprClientBuilder ()). buildPreviewClient ()) {
System . out . println ( "Using preview client..." );
tryLock ( client );
unlock ( client );
}
}
/**
* 尝试获取锁。
*
* @param client DaprPreviewClient 对象
*/
public static void tryLock ( DaprPreviewClient client ) {
System . out . println ( "*******trying to get a free distributed lock********" );
try {
LockRequest lockRequest = new LockRequest ( LOCK_STORE_NAME , "resouce1" , "owner1" , 5 );
Mono < Boolean > result = client . tryLock ( lockRequest );
System . out . println ( "Lock result -> " + ( Boolean . TRUE . equals ( result . block ()) ? "SUCCESS" : "FAIL" ));
} catch ( Exception ex ) {
System . out . println ( ex . getMessage ());
}
}
/**
* 解锁。
*
* @param client DaprPreviewClient 对象
*/
public static void unlock ( DaprPreviewClient client ) {
System . out . println ( "*******unlock a distributed lock********" );
try {
UnlockRequest unlockRequest = new UnlockRequest ( LOCK_STORE_NAME , "resouce1" , "owner1" );
Mono < UnlockResponseStatus > result = client . unlock ( unlockRequest );
System . out . println ( "Unlock result ->" + result . block (). name ());
} catch ( Exception ex ) {
System . out . println ( ex . getMessage ());
}
}
}
工作流 package io.dapr.examples.workflows ;
import io.dapr.workflows.client.DaprWorkflowClient ;
import io.dapr.workflows.client.WorkflowState ;
import java.time.Duration ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeoutException ;
/**
* 有关设置说明,请参阅 README。
*/
public class DemoWorkflowClient {
/**
* 主方法。
*
* @param args 输入参数(未使用)。
* @throws InterruptedException 如果程序已中断。
*/
public static void main ( String [] args ) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient ();
try ( client ) {
String separatorStr = "*******" ;
System . out . println ( separatorStr );
String instanceId = client . scheduleNewWorkflow ( DemoWorkflow . class , "input data" );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**GetWorkflowMetadata:Running Workflow**" );
WorkflowState workflowMetadata = client . getWorkflowState ( instanceId , true );
System . out . printf ( "Result: %s%n" , workflowMetadata );
System . out . println ( separatorStr );
System . out . println ( "**WaitForWorkflowStart**" );
try {
WorkflowState waitForWorkflowStartResult =
client . waitForWorkflowStart ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForWorkflowStartResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForWorkflowStart has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**SendExternalMessage**" );
client . raiseEvent ( instanceId , "TestEvent" , "TestEventPayload" );
System . out . println ( separatorStr );
System . out . println ( "** Registering parallel Events to be captured by allOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "event1" , "TestEvent 1 Payload" );
client . raiseEvent ( instanceId , "event2" , "TestEvent 2 Payload" );
client . raiseEvent ( instanceId , "event3" , "TestEvent 3 Payload" );
System . out . printf ( "Events raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "** Registering Event to be captured by anyOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "e2" , "event 2 Payload" );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**waitForWorkflowCompletion**" );
try {
WorkflowState waitForWorkflowCompletionResult =
client . waitForWorkflowCompletion ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForWorkflowCompletionResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForWorkflowCompletion has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**purgeWorkflow**" );
boolean purgeResult = client . purgeWorkflow ( instanceId );
System . out . printf ( "purgeResult: %s%n" , purgeResult );
System . out . println ( separatorStr );
System . out . println ( "**raiseEvent**" );
String eventInstanceId = client . scheduleNewWorkflow ( DemoWorkflow . class );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , eventInstanceId );
client . raiseEvent ( eventInstanceId , "TestException" , null );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , eventInstanceId );
System . out . println ( separatorStr );
String instanceToTerminateId = "terminateMe" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , instanceToTerminateId );
System . out . printf ( "Started new workflow instance with specified ID: %s%n" , instanceToTerminateId );
TimeUnit . SECONDS . sleep ( 5 );
System . out . println ( "Terminate this workflow instance manually before the timeout is reached" );
client . terminateWorkflow ( instanceToTerminateId , null );
System . out . println ( separatorStr );
String restartingInstanceId = "restarting" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , restartingInstanceId );
System . out . printf ( "Started new workflow instance with ID: %s%n" , restartingInstanceId );
System . out . println ( "Sleeping 30 seconds to restart the workflow" );
TimeUnit . SECONDS . sleep ( 30 );
System . out . println ( "**SendExternalMessage: RestartEvent**" );
client . raiseEvent ( restartingInstanceId , "RestartEvent" , "RestartEventPayload" );
System . out . println ( "Sleeping 30 seconds to terminate the eternal workflow" );
TimeUnit . SECONDS . sleep ( 30 );
client . terminateWorkflow ( restartingInstanceId , null );
}
System . out . println ( "Exiting DemoWorkflowClient." );
System . exit ( 0 );
}
}
边车 API 等待边车 DaprClient 还提供了一个辅助方法来等待边车变得健康(仅限组件)。使用此方法时,请务必指定超时时间(以毫秒为单位)并使用 block() 来等待响应式操作的结果。
// 在尝试使用 Dapr 组件之前,等待 Dapr 边车报告健康。
try ( DaprClient client = new DaprClientBuilder (). build ()) {
System . out . println ( "Waiting for Dapr sidecar ..." );
client . waitForSidecar ( 10000 ). block (); // 以毫秒为单位指定超时时间
System . out . println ( "Dapr sidecar is ready." );
...
}
// 在此处执行 Dapr 组件操作,例如获取密钥或保存状态。
关闭边车 try ( DaprClient client = new DaprClientBuilder (). build ()) {
logger . info ( "Sending shutdown request." );
client . shutdown (). block ();
logger . info ( "Ensuring dapr has stopped." );
...
}
了解更多有关可添加到 Java 应用程序的 Dapr Java SDK 包 的信息。
安全 应用 API 令牌身份验证 像发布订阅、输入绑定或作业这样的构建块需要 Dapr 向您的应用程序发出传入调用,您可以使用 Dapr 应用 API 令牌身份验证 来保护这些请求。这确保只有 Dapr 可以调用您应用程序的端点。
了解两种令牌 Dapr 使用两种不同的令牌来保护通信。有关这两种令牌的详细信息,请参阅属性 :
DAPR_API_TOKEN (您的应用 → Dapr 边车):使用 DaprClient 时由 Java SDK 自动处理APP_API_TOKEN (Dapr → 您的应用):需要在应用程序中进行服务器端验证下面的示例展示如何为 APP_API_TOKEN 实施服务器端验证。
实施服务器端令牌验证 使用 gRPC 协议时,实施服务器拦截器来捕获元数据。
import io.grpc.Context ;
import io.grpc.Contexts ;
import io.grpc.Metadata ;
import io.grpc.ServerCall ;
import io.grpc.ServerCallHandler ;
import io.grpc.ServerInterceptor ;
public class SubscriberGrpcService extends AppCallbackGrpc . AppCallbackImplBase {
public static final Context . Key < Metadata > METADATA_KEY = Context . key ( "grpc-metadata" );
// gRPC 拦截器来捕获元数据
public static class MetadataInterceptor implements ServerInterceptor {
@Override
public < ReqT , RespT > ServerCall . Listener < ReqT > interceptCall (
ServerCall < ReqT , RespT > call ,
Metadata headers ,
ServerCallHandler < ReqT , RespT > next ) {
Context contextWithMetadata = Context . current (). withValue ( METADATA_KEY , headers );
return Contexts . interceptCall ( contextWithMetadata , call , headers , next );
}
}
// 您的服务方法放在这里...
}
在构建 gRPC 服务器时注册拦截器:
Server server = ServerBuilder . forPort ( port )
. intercept ( new SubscriberGrpcService . MetadataInterceptor ())
. addService ( new SubscriberGrpcService ())
. build ();
server . start ();
然后,在您的服务方法中,从元数据中提取令牌:
@Override
public void onTopicEvent ( DaprAppCallbackProtos . TopicEventRequest request ,
StreamObserver < DaprAppCallbackProtos . TopicEventResponse > responseObserver ) {
try {
// 从上下文中提取元数据
Context context = Context . current ();
Metadata metadata = METADATA_KEY . get ( context );
if ( metadata != null ) {
String apiToken = metadata . get (
Metadata . Key . of ( "dapr-api-token" , Metadata . ASCII_STRING_MARSHALLER ));
// 相应地验证令牌
}
// 处理请求
// ...
} catch ( Throwable e ) {
responseObserver . onError ( e );
}
}
与 HTTP 端点一起使用 对于基于 HTTP 的端点,从标头中提取令牌:
@RestController
public class SubscriberController {
@PostMapping ( path = "/endpoint" )
public Mono < Void > handleRequest (
@RequestBody ( required = false ) byte [] body ,
@RequestHeader Map < String , String > headers ) {
return Mono . fromRunnable (() -> {
try {
// 从标头中提取令牌
String apiToken = headers . get ( "dapr-api-token" );
// 相应地验证令牌
// 处理请求
} catch ( Exception e ) {
throw new RuntimeException ( e );
}
});
}
}
示例 有关使用发布订阅、绑定和作业的工作示例:
相关链接 有关 SDK 属性的完整列表以及如何配置它们,请访问属性 。
3.2.1 - 属性 用于配置 Dapr Java SDK 的全局属性,支持环境变量和系统属性
属性 Dapr Java SDK 提供了一组控制 SDK 行为的全局属性。这些属性可以通过环境变量或系统属性进行配置。系统属性可以在运行 Java 应用程序时使用 -D 标志进行设置。
这些属性会影响整个 SDK,包括客户端和运行时。它们控制的方面包括:
边车连接(端点、端口) 安全设置(TLS、API 令牌) 性能调优(超时、连接池) 协议设置(gRPC、HTTP) 字符串编码 环境变量 以下环境变量可用于配置 Dapr Java SDK:
边车端点 当设置这些变量时,客户端将自动使用它们连接到 Dapr 边车。
环境变量 描述 默认值 DAPR_GRPC_ENDPOINTDapr 边车的 gRPC 端点 localhost:50001DAPR_HTTP_ENDPOINTDapr 边车的 HTTP 端点 localhost:3500DAPR_GRPC_PORTDapr 边车的 gRPC 端口(已弃用,DAPR_GRPC_ENDPOINT 优先级更高) 50001DAPR_HTTP_PORTDapr 边车的 HTTP 端口(已弃用,DAPR_HTTP_ENDPOINT 优先级更高) 3500
API 令牌 Dapr 支持两种类型的 API 令牌来保护通信:
环境变量 描述 默认值 DAPR_API_TOKEN用于验证从你的应用向 Dapr 边车发起 请求的 API 令牌。当使用 DaprClient 时,Java SDK 会自动在请求中包含此令牌。 nullAPP_API_TOKEN用于验证从 Dapr 向你的应用发起 请求的 API 令牌。当设置此变量时,Dapr 在调用你的应用程序时(例如发布订阅订阅者、输入绑定或任务触发器),会在 dapr-api-token 请求头/元数据中包含此令牌。你的应用程序必须验证此令牌。 null
有关实现示例,请参阅 App API Token Authentication 。更多详情请参阅 Dapr API token authentication 。
gRPC 配置 TLS 设置 为了安全地进行 gRPC 通信,你可以使用以下环境变量配置 TLS 设置:
环境变量 描述 默认值 DAPR_GRPC_TLS_INSECURE当设置为 “true” 时,启用不安全的 TLS 模式,该模式仍然使用 TLS 但不验证证书。这会使用 InsecureTrustManagerFactory 来信任所有证书。应仅用于测试或安全环境。 falseDAPR_GRPC_TLS_CA_PATHCA 证书文件的路径。用于与具有自签名证书的服务器建立 TLS 连接。 nullDAPR_GRPC_TLS_CERT_PATH用于客户端认证的 TLS 证书文件路径。 nullDAPR_GRPC_TLS_KEY_PATH用于客户端认证的 TLS 私钥文件路径。 null
Keepalive 设置 使用以下环境变量配置 gRPC keepalive 行为:
环境变量 描述 默认值 DAPR_GRPC_ENABLE_KEEP_ALIVE是否启用 gRPC keepalive falseDAPR_GRPC_KEEP_ALIVE_TIME_SECONDSgRPC keepalive 时间(秒) 10DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDSgRPC keepalive 超时(秒) 5DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS是否在没有调用时保持 gRPC 连接存活 true
入站消息设置 使用以下环境变量配置 gRPC 入站消息设置:
环境变量 描述 默认值 DAPR_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTESDapr 的 gRPC 最大入站消息大小(字节)。此值设置应用程序可以接收的 gRPC 消息的最大大小 4194304DAPR_GRPC_MAX_INBOUND_METADATA_SIZE_BYTESDapr 的 gRPC 最大入站元数据大小(字节) 8192
HTTP 客户端配置 这些属性控制用于与 Dapr 边车通信的 HTTP 客户端的行为:
环境变量 描述 默认值 DAPR_HTTP_CLIENT_READ_TIMEOUT_SECONDSHTTP 客户端读取操作的超时时间(秒)。这是等待 Dapr 边车响应的最长时间。 60DAPR_HTTP_CLIENT_MAX_REQUESTS可以同时执行的最大 HTTP 请求数。超过此限制后,请求将在内存中排队等待正在运行的调用完成。 1024DAPR_HTTP_CLIENT_MAX_IDLE_CONNECTIONSHTTP 连接池中的最大空闲连接数。这是池中可以保持空闲的最大连接数。 128
API 配置 这些属性控制通过 SDK 发起的 API 调用的行为:
环境变量 描述 默认值 DAPR_API_MAX_RETRIES向 Dapr 边车发起 API 调用时,可重试异常的最大重试次数 0DAPR_API_TIMEOUT_MILLISECONDS向 Dapr 边车发起 API 调用的超时时间(毫秒)。值为 0 表示无超时。 0
字符串编码 环境变量 描述 默认值 DAPR_STRING_CHARSETSDK 中用于字符串编码/解码的字符集。必须是有效的 Java 字符集名称。 UTF-8
系统属性 所有环境变量都可以使用 -D 标志设置为系统属性。以下是可用的系统属性的完整列表:
系统属性 描述 默认值 dapr.sidecar.ipDapr 边车的 IP 地址 localhostdapr.http.portDapr 边车的 HTTP 端口 3500dapr.grpc.portDapr 边车的 gRPC 端口 50001dapr.grpc.tls.cert.pathgRPC TLS 证书的路径 nulldapr.grpc.tls.key.pathgRPC TLS 密钥的路径 nulldapr.grpc.tls.ca.pathgRPC TLS CA 证书的路径 nulldapr.grpc.tls.insecure是否使用不安全的 TLS 模式 falsedapr.grpc.endpoint远程边车的 gRPC 端点 nulldapr.grpc.enable.keep.alive是否启用 gRPC keepalive falsedapr.grpc.keep.alive.time.secondsgRPC keepalive 时间(秒) 10dapr.grpc.keep.alive.timeout.secondsgRPC keepalive 超时(秒) 5dapr.grpc.keep.alive.without.calls是否在没有调用时保持 gRPC 连接存活 truedapr.http.endpoint远程边车的 HTTP 端点 nulldapr.api.maxRetriesAPI 调用的最大重试次数 0dapr.api.timeoutMillisecondsAPI 调用的超时时间(毫秒) 0dapr.api.token用于身份验证的 API 令牌 nulldapr.string.charsetSDK 中使用的字符串编码 UTF-8dapr.http.client.readTimeoutSecondsHTTP 客户端读取的超时时间(秒) 60dapr.http.client.maxRequests最大并发 HTTP 请求数 1024dapr.http.client.maxIdleConnections最大空闲 HTTP 连接数 128
属性解析顺序 属性按以下顺序解析:
覆盖值(如果在创建 Properties 实例时提供) 系统属性(通过 -D 设置) 环境变量 默认值 SDK 按顺序检查每个来源。如果某个值对属性类型无效(例如,数值属性为非数字),SDK 将记录警告并尝试下一个来源。例如:
# 无效的布尔值 - 将被忽略
java -Ddapr.grpc.enable.keep.alive= not-a-boolean -jar myapp.jar
# 有效的布尔值 - 将被使用
export DAPR_GRPC_ENABLE_KEEP_ALIVE = false
在这种情况下,将使用环境变量,因为系统属性值无效。但是,如果两个值都有效,系统属性优先:
# 有效的布尔值 - 将被使用
java -Ddapr.grpc.enable.keep.alive= true -jar myapp.jar
# 有效的布尔值 - 将被忽略
export DAPR_GRPC_ENABLE_KEEP_ALIVE = false
可以通过 DaprClientBuilder 以两种方式设置覆盖值:
使用单个属性覆盖(大多数情况下推荐): import io.dapr.config.Properties ;
// 设置单个属性覆盖
DaprClient client = new DaprClientBuilder ()
. withPropertyOverride ( Properties . GRPC_ENABLE_KEEP_ALIVE , "true" )
. build ();
// 或设置多个属性覆盖
DaprClient client = new DaprClientBuilder ()
. withPropertyOverride ( Properties . GRPC_ENABLE_KEEP_ALIVE , "true" )
. withPropertyOverride ( Properties . HTTP_CLIENT_READ_TIMEOUT_SECONDS , "120" )
. build ();
使用 Properties 实例(当你需要一次设置多个属性时很有用): // 创建属性覆盖的映射
Map < String , String > overrides = new HashMap <> ();
overrides . put ( "dapr.grpc.enable.keep.alive" , "true" );
overrides . put ( "dapr.http.client.readTimeoutSeconds" , "120" );
// 创建带有覆盖的 Properties 实例
Properties properties = new Properties ( overrides );
// 在创建客户端时使用这些属性
DaprClient client = new DaprClientBuilder ()
. withProperties ( properties )
. build ();
对于大多数用例,你会使用系统属性或环境变量。覆盖值主要用于在同一应用程序中需要为 SDK 的不同实例设置不同的属性值时。
代理配置 你可以使用系统属性为 Java 应用程序配置代理设置。这些是 Java 网络层(java.net 包)的标准 Java 系统属性,并非 Dapr 特有。它们会被 Java 的网络栈使用,包括 Dapr SDK 使用的 HTTP 客户端。
有关 Java 代理配置的详细信息,包括所有可用属性及其用法,请参阅 Java Networking Properties documentation 。
例如,以下是配置代理的方法:
# 配置 HTTP 代理 - 替换为你的实际代理服务器详细信息
java -Dhttp.proxyHost= your-proxy-server.com -Dhttp.proxyPort= 8080 -jar myapp.jar
# 配置 HTTPS 代理 - 替换为你的实际代理服务器详细信息
java -Dhttps.proxyHost= your-proxy-server.com -Dhttps.proxyPort= 8443 -jar myapp.jar
将 your-proxy-server.com 替换为你的实际代理服务器主机名或 IP 地址,并调整端口号以匹配你的代理服务器配置。
这些代理设置会影响 Java 应用程序发起的所有 HTTP/HTTPS 连接,包括与 Dapr 边车的连接。
3.3 - 工作流 如何开始使用 Dapr 工作流扩展
3.3.1 - 操作指南:在 Java SDK 中编写和管理 Dapr 工作流 如何使用 Dapr Java SDK 快速上手工作流
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例 ,你将:
本示例使用自托管模式 下通过 dapr init 初始化的默认配置。
前置条件 设置环境 克隆 Java SDK 仓库并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行此工作流示例所需的要求。
从 Java SDK 根目录进入 Dapr Workflow 示例。
运行 DemoWorkflowWorker DemoWorkflowWorker 类在 Dapr 的工作流运行时引擎中注册 DemoWorkflow 的实现。在 DemoWorkflowWorker.java 文件中,你可以找到 DemoWorkflowWorker 类和 main 方法:
public class DemoWorkflowWorker {
public static void main ( String [] args ) throws Exception {
// 在运行时中注册工作流。
WorkflowRuntime . getInstance (). registerWorkflow ( DemoWorkflow . class );
System . out . println ( "Start workflow runtime" );
WorkflowRuntime . getInstance (). startAndBlock ();
System . exit ( 0 );
}
}
在上述代码中:
WorkflowRuntime.getInstance().registerWorkflow() 将 DemoWorkflow 作为工作流注册到 Dapr Workflow 运行时中。WorkflowRuntime.getInstance().start() 在 Dapr Workflow 运行时内构建并启动引擎。在终端中,执行以下命令以启动 DemoWorkflowWorker:
dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowWorker
预期输出
You're up and running! Both Dapr and your app logs will appear here.
...
== APP == Start workflow runtime
== APP == Sep 13, 2023 9:02:03 AM com.microsoft.durabletask.DurableTaskGrpcWorker startAndBlock
== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001.
运行 DemoWorkflowClient DemoWorkflowClient 启动已注册到 Dapr 的工作流实例。
public class DemoWorkflowClient {
// ...
public static void main ( String [] args ) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient ();
try ( client ) {
String separatorStr = "*******" ;
System . out . println ( separatorStr );
String instanceId = client . scheduleNewWorkflow ( DemoWorkflow . class , "input data" );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**GetInstanceMetadata:Running Workflow**" );
WorkflowState workflowMetadata = client . getWorkflowState ( instanceId , true );
System . out . printf ( "Result: %s%n" , workflowMetadata );
System . out . println ( separatorStr );
System . out . println ( "**WaitForWorkflowStart**" );
try {
WorkflowState waitForWorkflowStartResult =
client . waitForWorkflowStart ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForWorkflowStartResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForWorkflowStart has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**SendExternalMessage**" );
client . raiseEvent ( instanceId , "TestEvent" , "TestEventPayload" );
System . out . println ( separatorStr );
System . out . println ( "** Registering parallel Events to be captured by allOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "event1" , "TestEvent 1 Payload" );
client . raiseEvent ( instanceId , "event2" , "TestEvent 2 Payload" );
client . raiseEvent ( instanceId , "event3" , "TestEvent 3 Payload" );
System . out . printf ( "Events raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "** Registering Event to be captured by anyOf(t1,t2,t3) **" );
client . raiseEvent ( instanceId , "e2" , "event 2 Payload" );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , instanceId );
System . out . println ( separatorStr );
System . out . println ( "**waitForWorkflowCompletion**" );
try {
WorkflowState waitForWorkflowCompletionResult =
client . waitForWorkflowCompletion ( instanceId , Duration . ofSeconds ( 60 ), true );
System . out . printf ( "Result: %s%n" , waitForWorkflowCompletionResult );
} catch ( TimeoutException ex ) {
System . out . printf ( "waitForWorkflowCompletion has an exception:%s%n" , ex );
}
System . out . println ( separatorStr );
System . out . println ( "**purgeWorkflow**" );
boolean purgeResult = client . purgeWorkflow ( instanceId );
System . out . printf ( "purgeResult: %s%n" , purgeResult );
System . out . println ( separatorStr );
System . out . println ( "**raiseEvent**" );
String eventInstanceId = client . scheduleNewWorkflow ( DemoWorkflow . class );
System . out . printf ( "Started new workflow instance with random ID: %s%n" , eventInstanceId );
client . raiseEvent ( eventInstanceId , "TestException" , null );
System . out . printf ( "Event raised for workflow with instanceId: %s\n" , eventInstanceId );
System . out . println ( separatorStr );
String instanceToTerminateId = "terminateMe" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , instanceToTerminateId );
System . out . printf ( "Started new workflow instance with specified ID: %s%n" , instanceToTerminateId );
TimeUnit . SECONDS . sleep ( 5 );
System . out . println ( "Terminate this workflow instance manually before the timeout is reached" );
client . terminateWorkflow ( instanceToTerminateId , null );
System . out . println ( separatorStr );
String restartingInstanceId = "restarting" ;
client . scheduleNewWorkflow ( DemoWorkflow . class , null , restartingInstanceId );
System . out . printf ( "Started new workflow instance with ID: %s%n" , restartingInstanceId );
System . out . println ( "Sleeping 30 seconds to restart the workflow" );
TimeUnit . SECONDS . sleep ( 30 );
System . out . println ( "**SendExternalMessage: RestartEvent**" );
client . raiseEvent ( restartingInstanceId , "RestartEvent" , "RestartEventPayload" );
System . out . println ( "Sleeping 30 seconds to terminate the eternal workflow" );
TimeUnit . SECONDS . sleep ( 30 );
client . terminateWorkflow ( restartingInstanceId , null );
}
System . out . println ( "Exiting DemoWorkflowClient." );
System . exit ( 0 );
}
}
在第二个终端窗口中,运行以下命令以启动工作流:
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowClient
预期输出
*******
Started new workflow instance with random ID: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**GetInstanceMetadata:Running Workflow**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**WaitForWorkflowStart**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**SendExternalMessage**
*******
** Registering parallel Events to be captured by allOf(t1,t2,t3) **
Events raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
** Registering Event to be captured by anyOf(t1,t2,t3) **
Event raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**WaitForWorkflowCompletion**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: FAILED, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:55.054Z, Input: '"input data"', Output: '']
*******
**purgeWorkflow**
purgeResult: true
*******
**raiseEvent**
Started new workflow instance with random ID: 7707d141-ebd0-4e54-816e-703cb7a52747
Event raised for workflow with instanceId: 7707d141-ebd0-4e54-816e-703cb7a52747
*******
Started new workflow instance with specified ID: terminateMe
Terminate this workflow instance manually before the timeout is reached
*******
Started new workflow instance with ID: restarting
Sleeping 30 seconds to restart the workflow
**SendExternalMessage: RestartEvent**
Sleeping 30 seconds to terminate the eternal workflow
Exiting DemoWorkflowClient.
发生了什么? 当你运行 dapr run 时,工作流 worker 将工作流(DemoWorkflow)及其活动注册到 Dapr Workflow 引擎中。 当你运行 java 时,工作流客户端通过以下活动启动了工作流实例。你可以在运行 dapr run 的终端中查看相应的输出。工作流启动,引发三个并行任务,并等待它们完成。 工作流客户端调用活动,并将 “Hello Activity” 消息发送到控制台。 工作流超时并被清除。 工作流客户端使用随机 ID 启动新的工作流实例,使用另一个名为 terminateMe 的工作流实例将其终止,并使用名为 restarting 的工作流重启它。 然后工作流客户端退出。 后续步骤 高级特性 任务执行键 任务执行键是由 durabletask-java 库生成的唯一标识符。它们存储在 WorkflowActivityContext 中,可用于跟踪和管理工作流活动的执行。它们特别适用于:
幂等性 :确保同一任务的活动不会被执行多次状态管理 :跟踪活动执行的状态错误处理 :以受控方式管理重试和失败以下是在工作流活动中使用任务执行键的示例:
public class TaskExecutionKeyActivity implements WorkflowActivity {
@Override
public Object run ( WorkflowActivityContext ctx ) {
// 获取此活动的任务执行键
String taskExecutionKey = ctx . getTaskExecutionKey ();
// 使用该键来实现幂等性或状态管理
// 例如,检查此任务是否已执行
if ( isTaskAlreadyExecuted ( taskExecutionKey )) {
return getPreviousResult ( taskExecutionKey );
}
// 执行活动逻辑
Object result = executeActivityLogic ();
// 使用任务执行键存储结果
storeResult ( taskExecutionKey , result );
return result ;
}
}
3.4 - 作业 借助 Dapr Jobs 包,您可以从 Java 应用程序与 Dapr Jobs API 交互,以触发未来按照预定计划运行的操作,可选择是否携带有效负载。要开始使用,请浏览
Dapr Jobs 操作指南。
3.4.1 - 如何:使用 Java SDK 编写和管理 Dapr Jobs 如何使用 Dapr Java SDK 快速上手 Jobs
在本演示中,我们将调度一个 Dapr Job。调度的 Job 将触发同一应用中注册的端点。使用提供的 Jobs 示例 ,你将:
本示例使用自托管模式 下 dapr init 的默认配置。
前置条件 设置环境 克隆 Java SDK 仓库 并进入该目录。
git clone https://github.com/dapr/java-sdk.git
cd java-sdk
运行以下命令以安装使用 Dapr Java SDK 运行 jobs 示例所需的要求。
mvn clean install -DskipTests
从 Java SDK 根目录进入示例目录。
运行 Dapr sidecar。
dapr run --app-id jobsapp --dapr-grpc-port 51439 --dapr-http-port 3500 --app-port 8080
现在,Dapr 正在 http://localhost:3500 监听 HTTP 请求,在 http://localhost:51439 监听内部 Jobs gRPC 请求。
调度和获取 Job 在 DemoJobsClient 中有调度 Job 的步骤。使用 DaprPreviewClient 调用 scheduleJob
将向 Dapr Runtime 调度一个 Job。
public class DemoJobsClient {
/**
* The main method of this app to schedule and get jobs.
*/
public static void main ( String [] args ) throws Exception {
try ( DaprPreviewClient client = new DaprClientBuilder (). withPropertyOverrides ( overrides ). buildPreviewClient ()) {
// Schedule a job.
System . out . println ( "**** Scheduling a Job with name dapr-jobs-1 *****" );
ScheduleJobRequest scheduleJobRequest = new ScheduleJobRequest ( "dapr-job-1" ,
JobSchedule . fromString ( "* * * * * *" )). setData ( "Hello World!" . getBytes ());
client . scheduleJob ( scheduleJobRequest ). block ();
System . out . println ( "**** Scheduling job dapr-jobs-1 completed *****" );
}
}
}
调用 getJob 以检索先前创建并调度的 Job 详细信息。
client.getJob(new GetJobRequest("dapr-job-1")).block()
使用以下命令运行 DemoJobsClient。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.jobs.DemoJobsClient
示例输出 **** Scheduling a Job with name dapr-jobs-1 *****
**** Scheduling job dapr-jobs-1 completed *****
**** Retrieving a Job with name dapr-jobs-1 *****
设置一个在 Job 触发时被调用的端点 DemoJobsSpringApplication 类启动一个 Spring Boot 应用,该应用注册 JobsController 中指定的端点
该端点充当对调度的 Job 请求的回调。
@RestController
public class JobsController {
/**
* Handles jobs callback from Dapr.
*
* @param jobName name of the job.
* @param payload data from the job if payload exists.
* @return Empty Mono.
*/
@PostMapping ( "/job/{jobName}" )
public Mono < Void > handleJob ( @PathVariable ( "jobName" ) String jobName ,
@RequestBody ( required = false ) byte [] payload ) {
System . out . println ( "Job Name: " + jobName );
System . out . println ( "Job Payload: " + new String ( payload ));
return Mono . empty ();
}
}
参数:
jobName:被触发的 Job 的名称。payload:与 Job 关联的可选负载数据(作为字节数组)。使用以下命令运行 Spring Boot 应用。
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.jobs.DemoJobsSpringApplication
示例输出 Job Name: dapr-job-1
Job Payload: Hello World!
删除一个调度的 Job public class DemoJobsClient {
/**
* The main method of this app deletes a job that was previously scheduled.
*/
public static void main ( String [] args ) throws Exception {
try ( DaprPreviewClient client = new DaprClientBuilder (). buildPreviewClient ()) {
// Delete a job.
System . out . println ( "**** Delete a Job with name dapr-jobs-1 *****" );
client . deleteJob ( new DeleteJobRequest ( "dapr-job-1" )). block ();
}
}
}
后续步骤 3.5 - Dapr 与 Spring Boot 入门 如何开始使用 Dapr 与 Spring Boot
通过结合 Dapr 和 Spring Boot,我们可以创建独立于基础设施的 Java 应用程序,这些应用程序可以部署到不同的环境中,支持广泛的本地和云提供商服务。
首先,我们将从一个涵盖 DaprClient 和 Testcontainers 集成的简单集成开始,然后使用 Spring 和 Spring Boot 机制和编程模型来利用底层的 Dapr API。这有助于团队移除连接到特定环境的基础设施(数据库、键值存储、消息代理、配置/密钥存储等)所需的客户端和驱动程序等依赖项。
将 Dapr 和 Spring Boot 集成添加到您的项目 如果您已经有 Spring Boot 应用程序,可以直接将以下依赖项添加到您的项目中:
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
您可以在此处找到最新发布的版本 。
通过添加这些依赖项,您可以:
在应用程序内自动装配 DaprClient 以供使用 使用 Spring Data 和 Messaging 抽象以及编程模型,这些模型在底层使用 Dapr API 通过依赖 Testcontainers 来引导 Dapr 控制平面服务和默认组件,从而改进您的内部开发循环 一旦这些依赖项在您的应用程序中,您就可以依赖 Spring Boot 自动配置来自动装配 DaprClient 实例:
@Autowired
private DaprClient daprClient ;
这将连接到默认的 Dapr gRPC 端点 localhost:50001,要求您在应用程序外部启动 Dapr。
注意 默认情况下,以下属性是为 DaprClient 和 DaprWorkflowClient 预配置的:
dapr.client.httpEndpoint = http://localhost
dapr.client.httpPort = 3500
dapr.client.grpcEndpoint = localhost
dapr.client.grpcPort = 50001
dapr.client.apiToken = <your remote api token>
这些值是默认使用的,但您可以在 application.properties 文件中覆盖它们以适应您的环境。请注意,同时支持 kebab-case 和 camelCase。
您可以在应用程序的任何位置使用 DaprClient 与 Dapr API 交互,例如从 REST 端点内部:
@RestController
public class DemoRestController {
@Autowired
private DaprClient daprClient ;
@PostMapping ( "/store" )
public void storeOrder ( @RequestBody Order order ){
daprClient . saveState ( "kvstore" , order . orderId (), order ). block ();
}
}
record Order ( String orderId , Integer amount ){}
如果您希望在 Spring Boot 应用程序外部避免管理 Dapr,可以依赖 Testcontainers 在应用程序旁边引导 Dapr 以用于开发目的。
为此,我们可以创建一个使用 Testcontainers 来引导使用 Dapr API 开发应用程序所需的所有内容的测试配置。
使用 Testcontainers 和 Dapr 集成,我们让 @TestConfiguration 为我们的应用程序引导 Dapr。
请注意,对于此示例,我们正在使用一个名为 kvstore 的 Statestore 组件配置 Dapr,该组件连接到也由 Testcontainers 引导的 PostgreSQL 实例。
@TestConfiguration ( proxyBeanMethods = false )
public class DaprTestContainersConfig {
@Bean
@ServiceConnection
public DaprContainer daprContainer ( Network daprNetwork , PostgreSQLContainer <?> postgreSQLContainer ){
return new DaprContainer ( "daprio/daprd:1.16.0-rc.5" )
. withAppName ( "producer-app" )
. withNetwork ( daprNetwork )
. withComponent ( new Component ( "kvstore" , "state.postgresql" , "v1" , STATE_STORE_PROPERTIES ))
. withComponent ( new Component ( "kvbinding" , "bindings.postgresql" , "v1" , BINDING_PROPERTIES ))
. dependsOn ( postgreSQLContainer );
}
}
在测试类路径中,您可以添加一个使用此配置进行测试的新 Spring Boot 应用程序:
@SpringBootApplication
public class TestProducerApplication {
public static void main ( String [] args ) {
SpringApplication
. from ( ProducerApplication :: main )
. with ( DaprTestContainersConfig . class )
. run ( args );
}
}
现在您可以使用以下命令启动应用程序:
运行此命令将启动应用程序,使用提供的测试配置,其中包括 Testcontainers 和 Dapr 集成。在日志中,您应该能够看到为您的应用程序启动了 daprd 和 placement 服务容器。
除了之前的配置(DaprTestContainersConfig)之外,您的测试不应该测试 Dapr 本身,只测试应用程序暴露的 REST 端点。
利用 Spring 和 Spring Boot 编程模型与 Dapr Java SDK 允许您与所有 Dapr 构建块 进行接口。
但是,如果您想利用 Spring 和 Spring Boot 编程模型,可以使用 dapr-spring-boot-starter 集成。
这包括 Spring Data(KeyValueTemplate 和 CrudRepository)的实现以及用于生产和消费消息的 DaprMessagingTemplate
(类似于 Spring Kafka 、Spring Pulsar 和 Spring AMQP for RabbitMQ )和 Dapr 工作流。
使用 Spring Data CrudRepository 和 KeyValueTemplate 您可以使用众所周知的 Spring Data 构造,这些构造依赖于基于 Dapr 的实现。
使用 Dapr,您不需要添加任何与基础设施相关的驱动程序或客户端,使您的 Spring 应用程序更轻量,并且与其运行的环境解耦。
在底层,这些实现使用 Dapr Statestore 和 Binding API。
配置参数 使用 Spring Data 抽象,您可以配置 Dapr 将使用哪些 statestore 和绑定来连接到可用的基础设施。
这可以通过设置以下属性来完成:
dapr.statestore.name = kvstore
dapr.statestore.binding = kvbinding
然后您可以像这样 @Autowire KeyValueTemplate 或 CrudRepository:
@RestController
@EnableDaprRepositories
public class OrdersRestController {
@Autowired
private OrderRepository repository ;
@PostMapping ( "/orders" )
public void storeOrder ( @RequestBody Order order ){
repository . save ( order );
}
@GetMapping ( "/orders" )
public Iterable < Order > getAll (){
return repository . findAll ();
}
}
其中 OrderRepository 在一个扩展 Spring Data CrudRepository 接口的接口中定义:
public interface OrderRepository extends CrudRepository < Order , String > {}
请注意,@EnableDaprRepositories 注释完成了在 CrudRespository 接口下连接 Dapr API 的所有魔术。
因为 Dapr 允许用户从同一个应用程序与不同的 StateStores 交互,作为用户,您需要提供以下 bean 作为 Spring Boot @Configuration:
@Configuration
@EnableConfigurationProperties ({ DaprStateStoreProperties . class })
public class ProducerAppConfiguration {
@Bean
public KeyValueAdapterResolver keyValueAdapterResolver ( DaprClient daprClient , ObjectMapper mapper , DaprStateStoreProperties daprStatestoreProperties ) {
String storeName = daprStatestoreProperties . getName ();
String bindingName = daprStatestoreProperties . getBinding ();
return new DaprKeyValueAdapterResolver ( daprClient , mapper , storeName , bindingName );
}
@Bean
public DaprKeyValueTemplate daprKeyValueTemplate ( KeyValueAdapterResolver keyValueAdapterResolver ) {
return new DaprKeyValueTemplate ( keyValueAdapterResolver );
}
}
使用 Spring Messaging 生产和消费事件 类似于 Spring Kafka、Spring Pulsar 和 Spring AMQP,您可以使用 DaprMessagingTemplate 将消息发布到配置的基础设施。要消费消息,您可以使用 @Topic 注释(很快将重命名为 @DaprListener)。
要发布事件/消息,您可以在 Spring 应用程序中 @Autowired DaprMessagingTemplate。
对于此示例,我们将发布 Order 事件,并将消息发送到名为 topic 的主题。
@Autowired
private DaprMessagingTemplate < Order > messagingTemplate ;
@PostMapping ( "/orders" )
public void storeOrder ( @RequestBody Order order ){
repository . save ( order );
messagingTemplate . send ( "topic" , order );
}
与 CrudRepository 类似,我们需要指定要使用哪个 PubSub 代理来发布和消费我们的消息。
因为使用 Dapr 您可以连接到多个 PubSub 代理,您需要提供以下 bean 以让 Dapr 知道您的 DaprMessagingTemplate 将使用哪个 PubSub 代理:
@Bean
public DaprMessagingTemplate < Order > messagingTemplate ( DaprClient daprClient ,
DaprPubSubProperties daprPubSubProperties ) {
return new DaprMessagingTemplate <> ( daprClient , daprPubSubProperties . getName ());
}
最后,因为 Dapr PubSub 需要在您的应用程序和 Dapr 之间建立双向连接,所以您需要使用几个参数扩展您的 Testcontainers 配置:
@Bean
@ServiceConnection
public DaprContainer daprContainer ( Network daprNetwork , PostgreSQLContainer <?> postgreSQLContainer , RabbitMQContainer rabbitMQContainer ){
return new DaprContainer ( "daprio/daprd:1.16.0-rc.5" )
. withAppName ( "producer-app" )
. withNetwork ( daprNetwork )
. withComponent ( new Component ( "kvstore" , "state.postgresql" , "v1" , STATE_STORE_PROPERTIES ))
. withComponent ( new Component ( "kvbinding" , "bindings.postgresql" , "v1" , BINDING_PROPERTIES ))
. withComponent ( new Component ( "pubsub" , "pubsub.rabbitmq" , "v1" , rabbitMqProperties ))
. withAppPort ( 8080 )
. withAppChannelAddress ( "host.testcontainers.internal" )
. dependsOn ( rabbitMQContainer )
. dependsOn ( postgreSQLContainer );
}
现在,在 Dapr 配置中,我们包含了一个 pubsub 组件,它将连接到由 Testcontainers 启动的 RabbitMQ 实例。
我们还设置了两个重要参数 .withAppPort(8080) 和 .withAppChannelAddress("host.testcontainers.internal"),这允许 Dapr
在代理中发布消息时联系回应用程序。
要监听事件/消息,您需要在应用程序中暴露一个负责接收消息的端点。
如果您暴露 REST 端点,可以使用 @Topic 注释让 Dapr 知道它也需要将事件/消息转发到哪里:
@PostMapping ( "subscribe" )
@Topic ( pubsubName = "pubsub" , name = "topic" )
public void subscribe ( @RequestBody CloudEvent < Order > cloudEvent ){
events . add ( cloudEvent );
}
在引导应用程序时,Dapr 将注册要转发到您的应用程序暴露的 subscribe 端点的消息订阅。
如果您正在为这些订阅者编写测试,您需要确保 Testcontainers 知道您的应用程序将在端口 8080 上运行,
因此使用 Testcontainers 启动的容器知道您的应用程序在哪里:
@BeforeAll
public static void setup (){
org . testcontainers . Testcontainers . exposeHostPorts ( 8080 );
}
您可以在此处查看并运行完整的示例源代码 。
后续步骤 了解有关可添加到您的 Java 应用程序的 Dapr Java SDK 包 的更多信息。
查看如何指南,使用 Spring Boot 和 Testcontainers 进行 Dapr 工作流以获得本地工作流开发体验 。
相关链接 3.5.1 - 操作指南:使用 Spring Boot 编写和管理 Dapr 工作流 如何使用 Spring Boot 集成快速上手工作流
遵循与 Spring Data 和 Spring Messaging 相同的方法,dapr-spring-boot-starter 为 Spring Boot 用户带来了 Dapr 工作流集成。
使用 Dapr 工作流,您可以在 Java 代码中定义复杂的编排(工作流)。Dapr Spring Boot Starter 通过将 Workflows 和 WorkflowActivitys 作为 Spring Bean 进行管理,使您的开发更加便捷。
为了启用自动 bean 发现,您需要在 @SpringBootApplication 上添加 @EnableDaprWorkflows 注解:
@SpringBootApplication
@EnableDaprWorkflows
public class MySpringBootApplication {
...
}
通过添加此注解,所有的 Workflows 和 WorkflowActivitys bean 都会被 Spring 自动发现并注册到工作流引擎中。
创建工作流和活动 在您的 Spring Boot 应用程序中,您可以定义任意数量的工作流。为此,您需要创建新的 Workflow 接口实现。
@Component
public class MyWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
<工作流逻辑>
};
}
}
在工作流定义内部,您可以执行服务间交互、调度定时器或接收外部事件。
通过将所有 WorkflowActivitys 作为托管 bean,您可以使用 Spring 的 @Autowired 机制来注入工作流活动实现其功能所需的任何 bean。例如 @RestTemplate:
@Component
public class MyWorkflowActivity implements WorkflowActivity {
@Autowired
private RestTemplate restTemplate;
创建和与工作流交互 要创建和与工作流实例交互,您可以使用同样支持 @Autowired 的 DaprWorkflowClient。
@Autowired
private DaprWorkflowClient daprWorkflowClient;
应用程序现在可以调度新的工作流实例并触发事件。
String instanceId = daprWorkflowClient.scheduleNewWorkflow(MyWorkflow.class, payload);
以及
daprWorkflowClient.raiseEvent(instanceId, "MyEvenet", event);
在此处查看完整示例
后续步骤和资源 查看 Baeldung 关于 Dapr 工作流和 Dapr 发布订阅的博客文章 ,其中包含完整的工作示例。
查看 Dapr 工作流文档 ,了解如何使用 Dapr 工作流的更多信息。
4 - JavaScript SDK 用于开发 Dapr 应用程序的 JavaScript SDK 软件包
一个用于在 JavaScript 和 TypeScript 中构建 Dapr 应用程序的客户端库。该客户端抽象了服务调用、状态管理、发布订阅、密钥管理等公开 Dapr API,并提供简单直观的 API 用于构建应用程序。
安装 要开始使用 JavaScript SDK,请从 NPM 安装 Dapr JavaScript SDK 软件包:
npm install --save @dapr/dapr
结构 Dapr JavaScript SDK 包含两个主要组件:
DaprServer :管理所有 Dapr 边车与应用程序之间的通信。DaprClient :管理所有应用程序与 Dapr 边车之间的通信。上述通信可配置为使用 gRPC 或 HTTP 协议。
快速入门 为了帮助您快速入门,请查看以下资源:
客户端 创建 JavaScript 客户端并与 Dapr 边车及其他 Dapr 应用程序交互(例如,发布事件、输出绑定支持等)。
服务器 创建 JavaScript 服务器并让 Dapr 边车与您的应用程序交互(例如,订阅事件、输入绑定支持等)。
Actors 创建具有状态、提醒/定时器和方法的虚拟 Actor。
示例 克隆 JavaScript SDK 源代码并试用一些示例以快速入门。
4.1 - JavaScript 客户端 SDK 用于开发 Dapr 应用的 JavaScript 客户端 SDK
简介 Dapr 客户端允许您与 Dapr 边车通信,并访问其面向客户端的功能,如发布事件、调用输出绑定、状态管理、密钥管理等。
前置条件 安装和导入 Dapr JS SDK 使用 npm 安装 SDK: 导入库: import { DaprClient , DaprServer , HttpMethod , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr 边车主机
const daprPort = "3500" ; // 此示例服务器的 Dapr 边车端口
const serverHost = "127.0.0.1" ; // 此示例服务器的应用主机
const serverPort = "50051" ; // 此示例服务器的应用端口
// HTTP 示例
const client = new DaprClient ({ daprHost , daprPort });
// GRPC 示例
const client = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocolEnum.GRPC });
运行 要运行示例,您可以使用两种不同的协议与 Dapr 边车交互:HTTP(默认)或 gRPC。
使用 HTTP(默认) import { DaprClient } from "@dapr/dapr" ;
const client = new DaprClient ({ daprHost , daprPort });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start
# 或,使用 npm script
npm run start:dapr-http
使用 gRPC 由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外的参数来实现。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
const client = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocol.GRPC });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start
# 或,使用 npm script
npm run start:dapr-grpc
环境变量 Dapr 边车端点 您可以使用 DAPR_HTTP_ENDPOINT 和 DAPR_GRPC_ENDPOINT 环境变量来分别设置 Dapr 边车的 HTTP 和 gRPC 端点。当设置这些变量时,构造函数的选项参数中不必设置 daprHost 和 daprPort,客户端会自动从提供的端点中解析它们。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
// 使用 HTTP,当设置了 DAPR_HTTP_ENDPOINT 时
const client = new DaprClient ();
// 使用 gRPC,当设置了 DAPR_GRPC_ENDPOINT 时
const client = new DaprClient ({ communicationProtocol : CommunicationProtocol.GRPC });
如果设置了环境变量,但向构造函数传递了 daprHost 和 daprPort 值,后者将优先于环境变量。
Dapr API 令牌 您可以使用 DAPR_API_TOKEN 环境变量来设置 Dapr API 令牌。当设置此变量时,构造函数的选项参数中不必设置 daprApiToken,客户端会自动获取它。
通用 增加主体大小 您可以使用 DaprClient 的选项来增加应用程序用于与边车通信的主体大小。
import { DaprClient , CommunicationProtocol } from "@dapr/dapr" ;
// 允许使用 10Mb 的主体大小
// 默认为 4Mb
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocol.HTTP ,
maxBodySizeMb : 10 ,
});
代理请求 通过代理请求,我们可以利用 Dapr 通过其边车架构带来的独特能力,如服务发现、日志记录等,使我们能够立即"升级"我们的 gRPC 服务。gRPC 代理的此功能在 社区会议 41 中进行了演示。
创建代理 要执行 gRPC 代理,只需调用 client.proxy.create() 方法创建代理:
// 像往常一样,为我们的 dapr 边车创建一个客户端
// 此客户端负责确保边车已启动、我们可以通信等
const clientSidecar = new DaprClient ({ daprHost , daprPort , communicationProtocol : CommunicationProtocol.GRPC });
// 创建一个允许我们使用 gRPC 代码的代理
const clientProxy = await clientSidecar . proxy . create < GreeterClient >( GreeterClient );
现在我们可以按照 GreeterClient 接口中定义的方法调用(在本例中,该接口来自 Hello World 示例 )
幕后(技术工作原理)
gRPC 服务在 Dapr 中启动。我们通过 --app-port 告诉 Dapr 此 gRPC 服务器运行在哪个端口,并使用 --app-id <APP_ID_HERE> 为其提供唯一的 Dapr 应用 ID 我们现在可以通过连接到边车的客户端调用 Dapr 边车 在调用 Dapr 边车时,我们提供一个名为 dapr-app-id 的元数据键,其值为在 Dapr 中启动的 gRPC 服务器(例如在我们的示例中为 server) Dapr 现在将调用转发到配置的 gRPC 服务器 构建块 JavaScript 客户端 SDK 允许您与所有 Dapr 构建块 交互,重点关注客户端到边车的功能。
调用 API 调用服务 import { DaprClient , HttpMethod } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const serviceAppId = "my-app-id" ;
const serviceMethod = "say-hello" ;
// POST 请求
const response = await client . invoker . invoke ( serviceAppId , serviceMethod , HttpMethod . POST , { hello : "world" });
// 带请求头的 POST 请求
const response = await client . invoker . invoke (
serviceAppId ,
serviceMethod ,
HttpMethod . POST ,
{ hello : "world" },
{ headers : { "X-User-ID" : "123" } },
);
// GET 请求
const response = await client . invoker . invoke ( serviceAppId , serviceMethod , HttpMethod . GET );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关服务调用的完整指南,请访问 操作指南:调用服务 。
状态管理 API 保存、获取和删除应用状态 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const serviceStoreName = "my-state-store-name" ;
// 保存状态
const response = await client . state . save (
serviceStoreName ,
[
{
key : "first-key-name" ,
value : "hello" ,
metadata : {
foo : "bar" ,
},
},
{
key : "second-key-name" ,
value : "world" ,
},
],
{
metadata : {
ttlInSeconds : "3" , // 这应该覆盖状态项中的 ttl
},
},
);
// 获取状态
const response = await client . state . get ( serviceStoreName , "first-key-name" );
// 批量获取状态
const response = await client . state . getBulk ( serviceStoreName , [ "first-key-name" , "second-key-name" ]);
// 状态事务
await client . state . transaction ( serviceStoreName , [
{
operation : "upsert" ,
request : {
key : "first-key-name" ,
value : "new-data" ,
},
},
{
operation : "delete" ,
request : {
key : "second-key-name" ,
},
},
]);
// 删除状态
const response = await client . state . delete ( serviceStoreName , "first-key-name" );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关状态操作的完整列表,请访问 操作指南:获取和保存状态 。
查询状态 API import { DaprClient } from "@dapr/dapr" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const res = await client . state . query ( "state-mongodb" , {
filter : {
OR : [
{
EQ : { "person.org" : "Dev Ops" },
},
{
AND : [
{
EQ : { "person.org" : "Finance" },
},
{
IN : { state : [ "CA" , "WA" ] },
},
],
},
],
},
sort : [
{
key : "state" ,
order : "DESC" ,
},
],
page : {
limit : 10 ,
},
});
console . log ( res );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
发布订阅 API 发布消息 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// 以 text/plain 格式向主题发布消息
// 注意,除非明确指定,内容类型是从消息类型推断的
const response = await client . pubsub . publish ( pubSubName , topic , "hello, world!" );
// 如果发布失败,response 包含错误
console . log ( response );
// 以 application/json 格式向主题发布消息
await client . pubsub . publish ( pubSubName , topic , { hello : "world" });
// 以纯文本形式发布 JSON 消息
const options = { contentType : "text/plain" };
await client . pubsub . publish ( pubSubName , topic , { hello : "world" }, options );
// 以 application/cloudevents+json 格式向主题发布消息
// 您也可以使用 cloudevent SDK 创建云事件 https://github.com/cloudevents/sdk-javascript
const cloudEvent = {
specversion : "1.0" ,
source : "/some/source" ,
type : "example" ,
id : "1234" ,
};
await client . pubsub . publish ( pubSubName , topic , cloudEvent );
// 以原始负载形式发布云事件
const options = { metadata : { rawPayload : true } };
await client . pubsub . publish ( pubSubName , topic , "hello, world!" , options );
// 以 text/plain 格式向主题发布多条消息
await client . pubsub . publishBulk ( pubSubName , topic , [ "message 1" , "message 2" , "message 3" ]);
// 以 application/json 格式向主题发布多条消息
await client . pubsub . publishBulk ( pubSubName , topic , [
{ hello : "message 1" },
{ hello : "message 2" },
{ hello : "message 3" },
]);
// 使用显式批量发布消息发布多条消息
const bulkPublishMessages = [
{
entryID : "entry-1" ,
contentType : "application/json" ,
event : { hello : "foo message 1" },
},
{
entryID : "entry-2" ,
contentType : "application/cloudevents+json" ,
event : { ... cloudEvent , data : "foo message 2" , datacontenttype : "text/plain" },
},
{
entryID : "entry-3" ,
contentType : "text/plain" ,
event : "foo message 3" ,
},
];
await client . pubsub . publishBulk ( pubSubName , topic , bulkPublishMessages );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
绑定 API 调用输出绑定 输出绑定
import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const bindingName = "my-binding-name" ;
const bindingOperation = "create" ;
const message = { hello : "world" };
const response = await client . binding . send ( bindingName , bindingOperation , message );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关输出绑定的完整指南,请访问 操作指南:使用绑定 。
密钥 API 检索密钥 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const secretStoreName = "my-secret-store" ;
const secretKey = "secret-key" ;
// 从密钥存储中检索单个密钥
const response = await client . secret . get ( secretStoreName , secretKey );
// 从密钥存储中检索所有密钥
const response = await client . secret . getBulk ( secretStoreName );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关密钥的完整指南,请访问 操作指南:检索密钥 。
配置 API 获取配置键 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort : process.env.DAPR_GRPC_PORT ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const config = await client . configuration . get ( "config-store" , [ "key1" , "key2" ]);
console . log ( config );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
示例输出:
{
items: {
key1: { key: 'key1', value: 'foo', version: '', metadata: {} },
key2: { key: 'key2', value: 'bar2', version: '', metadata: {} }
}
}
订阅配置更新 import { DaprClient } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort : process.env.DAPR_GRPC_PORT ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
// 订阅键 "key1" 和 "key2" 的配置存储更改
const stream = await client . configuration . subscribeWithKeys ( "config-store" , [ "key1" , "key2" ], async ( data ) => {
console . log ( "订阅收到来自配置存储的更新:" , data );
});
// 等待 60 秒并取消订阅。
await new Promise (( resolve ) => setTimeout ( resolve , 60000 ));
stream . stop ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
示例输出:
订阅收到来自配置存储的更新: {
items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } }
}
订阅收到来自配置存储的更新: {
items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } }
}
加密 API JavaScript SDK 中仅在 gRPC 客户端上支持加密 API。
import { createReadStream , createWriteStream } from "node:fs" ;
import { readFile , writeFile } from "node:fs/promises" ;
import { pipeline } from "node:stream/promises" ;
import { DaprClient , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50050" ; // 此示例服务器的 Dapr 边车端口
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
// 使用流加密和解密消息
await encryptDecryptStream ( client );
// 使用缓冲区加密和解密消息
await encryptDecryptBuffer ( client );
}
async function encryptDecryptStream ( client : DaprClient ) {
// 首先,加密消息
console . log ( "== 使用流加密消息" );
console . log ( "将 plaintext.txt 加密为 ciphertext.out" );
await pipeline (
createReadStream ( "plaintext.txt" ),
await client . crypto . encrypt ({
componentName : "crypto-local" ,
keyName : "symmetric256" ,
keyWrapAlgorithm : "A256KW" ,
}),
createWriteStream ( "ciphertext.out" ),
);
// 解密消息
console . log ( "== 使用流解密消息" );
console . log ( "将 ciphertext.out 解密为 plaintext.out" );
await pipeline (
createReadStream ( "ciphertext.out" ),
await client . crypto . decrypt ({
componentName : "crypto-local" ,
}),
createWriteStream ( "plaintext.out" ),
);
}
async function encryptDecryptBuffer ( client : DaprClient ) {
// 读取 "plaintext.txt" 以便我们有一些内容
const plaintext = await readFile ( "plaintext.txt" );
// 首先,加密消息
console . log ( "== 使用缓冲区加密消息" );
const ciphertext = await client . crypto . encrypt ( plaintext , {
componentName : "crypto-local" ,
keyName : "my-rsa-key" ,
keyWrapAlgorithm : "RSA" ,
});
await writeFile ( "test.out" , ciphertext );
// 解密消息
console . log ( "== 使用缓冲区解密消息" );
const decrypted = await client . crypto . decrypt ( ciphertext , {
componentName : "crypto-local" ,
});
// 内容应该相等
if ( plaintext . compare ( decrypted ) !== 0 ) {
throw new Error ( "解密的消息与原始消息不匹配" );
}
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关加密的完整指南,请访问 操作指南:加密 。
分布式锁 API 尝试锁定和解锁 API import { CommunicationProtocolEnum , DaprClient } from "@dapr/dapr" ;
import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse" ;
const daprHost = "127.0.0.1" ;
const daprPortDefault = "3500" ;
async function start() {
const client = new DaprClient ({ daprHost , daprPort });
const storeName = "redislock" ;
const resourceId = "resourceId" ;
const lockOwner = "owner1" ;
let expiryInSeconds = 1000 ;
console . log ( `在 ${ storeName } 、 ${ resourceId } 上获取锁,所有者: ${ lockOwner } ` );
const lockResponse = await client . lock . lock ( storeName , resourceId , lockOwner , expiryInSeconds );
console . log ( lockResponse );
console . log ( `在 ${ storeName } 、 ${ resourceId } 上解锁,所有者: ${ lockOwner } ` );
const unlockResponse = await client . lock . unlock ( storeName , resourceId , lockOwner );
console . log ( "解锁 API 响应: " + getResponseStatus ( unlockResponse . status ));
}
function getResponseStatus ( status : LockStatus ) {
switch ( status ) {
case LockStatus.Success :
return "Success" ;
case LockStatus.LockDoesNotExist :
return "LockDoesNotExist" ;
case LockStatus.LockBelongsToOthers :
return "LockBelongsToOthers" ;
default :
return "InternalError" ;
}
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
有关分布式锁的完整指南,请访问 操作指南:使用分布式锁 。
工作流 API 工作流管理 import { DaprClient } from "@dapr/dapr" ;
async function start() {
const client = new DaprClient ();
// 启动新的工作流实例
const instanceId = await client . workflow . start ( "OrderProcessingWorkflow" , {
Name : "Paperclips" ,
TotalCost : 99.95 ,
Quantity : 4 ,
});
console . log ( `已启动工作流实例 ${ instanceId } ` );
// 获取工作流实例
const workflow = await client . workflow . get ( instanceId );
console . log (
`工作流 ${ workflow . workflowName } ,创建于 ${ workflow . createdAt . toUTCString () } ,状态为 ${
workflow . runtimeStatus
} ` ,
);
console . log ( `其他属性: ${ JSON . stringify ( workflow . properties ) } ` );
// 暂停工作流实例
await client . workflow . pause ( instanceId );
console . log ( `已暂停工作流实例 ${ instanceId } ` );
// 恢复工作流实例
await client . workflow . resume ( instanceId );
console . log ( `已恢复工作流实例 ${ instanceId } ` );
// 终止工作流实例
await client . workflow . terminate ( instanceId );
console . log ( `已终止工作流实例 ${ instanceId } ` );
// 清除工作流实例
await client . workflow . purge ( instanceId );
console . log ( `已清除工作流实例 ${ instanceId } ` );
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
相关链接 4.2 - JavaScript Server SDK 用于开发 Dapr 应用程序的 JavaScript Server SDK
简介 Dapr Server 将允许您接收来自 Dapr 边车的通信,并访问其面向服务器的功能,例如:订阅事件、接收输入绑定等。
前置条件 安装和导入 Dapr 的 JS SDK 使用 npm 安装 SDK: 导入库: import { DaprServer , CommunicationProtocolEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server
// HTTP Example
const server = new DaprServer ({
serverHost ,
serverPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP , // DaprClient to use same communication protocol as DaprServer, in case DaprClient protocol not mentioned explicitly
clientOptions : {
daprHost ,
daprPort ,
},
});
// GRPC Example
const server = new DaprServer ({
serverHost ,
serverPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
clientOptions : {
daprHost ,
daprPort ,
},
});
运行 要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。
使用 HTTP(内置 express webserver) import { DaprServer } from "@dapr/dapr" ;
const server = new DaprServer ({
serverHost : appHost ,
serverPort : appPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server . start ();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start
# or, using npm script
npm run start:dapr-http
ℹ️ Note: The app-port is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.
使用 HTTP(自备 express webserver) 您不一定要使用 Dapr sidecar 的内置 web 服务器与应用程序通信,您也可以自带实例。这在构建 REST API 后端并希望直接集成 Dapr 的场景中非常有用。
注意,目前仅适用于 express 。
💡 Note: when using a custom web-server, the SDK will configure server properties like max body size, and add new routes to it. The routes are unique on their own to avoid any collisions with your application, but it’s not guaranteed to not collide.
import { DaprServer , CommunicationProtocolEnum } from "@dapr/dapr" ;
import express from "express" ;
const myApp = express ();
myApp . get ( "/my-custom-endpoint" , ( req , res ) => {
res . send ({ msg : "My own express app!" });
});
const daprServer = new DaprServer ({
serverHost : "127.0.0.1" , // App Host
serverPort : "50002" , // App Port
serverHttp : myApp ,
clientOptions : {
daprHost
daprPort
}
});
// Initialize subscriptions before the server starts, the Dapr sidecar uses it.
// This will also initialize the app server itself (removing the need for `app.listen` to be called).
await daprServer . start ();
配置好上述内容后,您可以像往常一样调用自定义端点:
const res = await fetch ( `http://127.0.0.1:50002/my-custom-endpoint` );
const json = await res . json ();
使用 gRPC 由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外参数来实现这一点。
import { DaprServer , CommunicationProtocol } from "@dapr/dapr" ;
const server = new DaprServer ({
serverHost : appHost ,
serverPort : appPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
clientOptions : {
daprHost ,
daprPort ,
},
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server . start ();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start
# or, using npm script
npm run start:dapr-grpc
ℹ️ Note: The app-port is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.
构建块 JavaScript Server SDK 允许您与所有 Dapr 构建块 进行交互,主要专注于 Sidecar 到应用程序的功能。
Invocation API Listen to an Invocation import { DaprServer , DaprInvokerCallbackContent } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const callbackFunction = ( data : DaprInvokerCallbackContent ) => {
console . log ( "Received body: " , data . body );
console . log ( "Received metadata: " , data . metadata );
console . log ( "Received query: " , data . query );
console . log ( "Received headers: " , data . headers ); // only available in HTTP
};
await server . invoker . listen ( "hello-world" , callbackFunction , { method : HttpMethod.GET });
// You can now invoke the service with your app id and method "hello-world"
await server . start ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
For a full guide on service invocation visit How-To: Invoke a service .
PubSub API Subscribe to messages 订阅消息可以通过多种方式完成,以提供在主题上接收消息的灵活性:
通过 subscribe 方法直接订阅 通过 subscribeWithOptions 方法带选项直接订阅 之后通过 susbcribeOnEvent 方法订阅 每次事件到达时,我们将其正文作为 data 传递,将标头作为 headers 传递,这些标头可能包含事件发布者的属性(例如,来自 IoT Hub 的设备 ID)
Dapr 要求在启动时设置订阅,但在 JS SDK 中,我们也允许之后添加事件处理程序,为您提供编程的灵活性。
下面提供了一个示例
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// Configure Subscriber for a Topic
// Method 1: Direct subscription through the `subscribe` method
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` ),
);
// Method 2: Direct susbcription with options through the `subscribeWithOptions` method
await server . pubsub . subscribeWithOptions ( pubSubName , topic , {
callback : async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` ),
});
// Method 3: Subscription afterwards through the `susbcribeOnEvent` method
// Note: we use default, since if no route was passed (empty options) we utilize "default" as the route name
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-1" , {});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "default" , async ( data : any , headers : object ) => {
console . log ( `Received Data: ${ JSON . stringify ( data ) } with headers: ${ JSON . stringify ( headers ) } ` );
});
// Start the server
await server . start ();
}
For a full list of state operations visit How-To: Publish & subscribe .
Subscribe with SUCCESS/RETRY/DROP status Dapr 支持重试逻辑的状态码 来指定消息处理后应该发生什么。
⚠️ JS SDK 允许在同一主题上设置多个回调,我们按 RETRY > DROP > SUCCESS 的顺序处理状态优先级,并默认为 SUCCESS
⚠️ 确保在应用程序中配置弹性 以处理 RETRY 消息
在 JS SDK 中,我们通过 DaprPubSubStatusEnum 枚举支持这些消息。为了确保 Dapr 将重试,我们还配置了弹性策略。
components/resiliency.yaml
apiVersion : dapr.io/v1alpha1
kind : Resiliency
metadata :
name : myresiliency
spec :
policies :
retries :
# Global Retry Policy for Inbound Component operations
DefaultComponentInboundRetryPolicy :
policy : constant
duration : 500ms
maxRetries : 10
targets :
components :
messagebus :
inbound :
retry : DefaultComponentInboundRetryPolicy
src/index.ts
import { DaprServer , DaprPubSubStatusEnum } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// Process a message successfully
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . SUCCESS ;
});
// Retry a message
// Note: this example will keep on retrying to deliver the message
// Note 2: each component can have their own retry configuration
// e.g., https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-redis-pubsub/
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . RETRY ;
});
// Drop a message
await server . pubsub . subscribe ( pubSubName , topic , async ( data : any , headers : object ) => {
return DaprPubSubStatusEnum . DROP ;
});
// Start the server
await server . start ();
}
Subscribe to messages rule based Dapr 支持根据规则将消息路由 到不同的处理程序(路由)。
例如,您正在编写一个需要根据消息的"type"处理消息的应用程序,使用 Dapr,您可以将它们发送到不同的路由 handlerType1 和 handlerType2,默认路由为 handlerDefault
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
const topic = "topic-a" ;
// Configure Subscriber for a Topic with rule set
// Note: the default route and match patterns are optional
await server . pubsub . subscribe ( "pubsub-redis" , "topic-1" , {
default : "/default" ,
rules : [
{
match : `event.type == "my-type-1"` ,
path : "/type-1" ,
},
{
match : `event.type == "my-type-2"` ,
path : "/type-2" ,
},
],
});
// Add handlers for each route
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "default" , async ( data ) => {
console . log ( `Handling Default` );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "type-1" , async ( data ) => {
console . log ( `Handling Type 1` );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-1" , "type-2" , async ( data ) => {
console . log ( `Handling Type 2` );
});
// Start the server
await server . start ();
}
Susbcribe with Wildcards 支持通配符 * 和 +(请确保验证 pubsub 组件是否支持它 ),可以按如下方式订阅:
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
// * Wildcard
await server . pubsub . subscribe ( pubSubName , "/events/*" , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } ` ),
);
// + Wildcard
await server . pubsub . subscribe ( pubSubName , "/events/+/temperature" , async ( data : any , headers : object ) =>
console . log ( `Received Data: ${ JSON . stringify ( data ) } ` ),
);
// Start the server
await server . start ();
}
Bulk Subscribe to messages 批量订阅受支持,可通过以下 API 使用:
通过 subscribeBulk 方法批量订阅:maxMessagesCount 和 maxAwaitDurationMs 是可选的;如果未提供,将使用相关组件的默认值。 在监听消息时,应用程序从 Dapr 批量接收消息。然而,与常规订阅一样,回调函数一次接收一条消息,用户可以选择返回 DaprPubSubStatusEnum 值来确认成功、重试或丢弃消息。默认行为是返回成功响应。
有关更多详细信息,请参阅本文档 。
import { DaprServer } from "@dapr/dapr" ;
const pubSubName = "orderPubSub" ;
const topic = "topicbulk" ;
const daprHost = process . env . DAPR_HOST || "127.0.0.1" ;
const daprHttpPort = process . env . DAPR_HTTP_PORT || "3502" ;
const serverHost = process . env . SERVER_HOST || "127.0.0.1" ;
const serverPort = process . env . APP_PORT || 5001 ;
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort : daprHttpPort ,
},
});
// Publish multiple messages to a topic with default config.
await client . pubsub . subscribeBulk ( pubSubName , topic , ( data ) =>
console . log ( "Subscriber received: " + JSON . stringify ( data )),
);
// Publish multiple messages to a topic with specific maxMessagesCount and maxAwaitDurationMs.
await client . pubsub . subscribeBulk (
pubSubName ,
topic ,
( data ) => {
console . log ( "Subscriber received: " + JSON . stringify ( data ));
return DaprPubSubStatusEnum . SUCCESS ; // If App doesn't return anything, the default is SUCCESS. App can also return RETRY or DROP based on the incoming message.
},
{
maxMessagesCount : 100 ,
maxAwaitDurationMs : 40 ,
},
);
}
Dead Letter Topics Dapr 支持死信主题 。这意味着当消息处理失败时,它将被发送到死信队列。例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed。
例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed。
您可以将以下选项与 subscribeWithOptions 方法一起使用:
deadletterTopic:指定死信主题名称(注意:如果未提供,我们将创建一个名为 deadletter 的主题)deadletterCallback:作为死信处理程序触发的方法在 JS SDK 中实现死信支持可以通过以下方式完成:
将 deadletterCallback 作为选项传递 通过 subscribeToRoute 手动订阅路由 下面提供了一个示例
import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ; // Dapr Sidecar Host
const daprPort = "3500" ; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1" ; // App Host of this Example Server
const serverPort = "50051" ; // App Port of this Example Server "
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const pubSubName = "my-pubsub-name" ;
// Method 1 (direct subscribing through subscribeWithOptions)
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-5" , {
callback : async ( data : any ) => {
throw new Error ( "Triggering Deadletter" );
},
deadLetterCallback : async ( data : any ) => {
console . log ( "Handling Deadletter message" );
},
});
// Method 2 (subscribe afterwards)
await server . pubsub . subscribeWithOptions ( "pubsub-redis" , "topic-options-1" , {
deadletterTopic : "my-deadletter-topic" ,
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "default" , async () => {
throw new Error ( "Triggering Deadletter" );
});
server . pubsub . subscribeToRoute ( "pubsub-redis" , "topic-options-1" , "my-deadletter-topic" , async () => {
console . log ( "Handling Deadletter message" );
});
// Start server
await server . start ();
}
Bindings API import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
const bindingName = "my-binding-name" ;
const response = await server . binding . receive ( bindingName , async ( data : any ) =>
console . log ( `Got Data: ${ JSON . stringify ( data ) } ` ),
);
await server . start ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
For a full guide on output bindings visit How-To: Use bindings .
Configuration API 💡 The configuration API is currently only available through gRPC
Getting a configuration value import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const config = await client . configuration . get ( "config-redis" , [ "myconfigkey1" , "myconfigkey2" ]);
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
Subscribing to Key Changes import { DaprServer } from "@dapr/dapr" ;
const daprHost = "127.0.0.1" ;
const daprPort = "3500" ;
const serverHost = "127.0.0.1" ;
const serverPort = "5051" ;
async function start() {
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.GRPC ,
});
const stream = await client . configuration . subscribeWithKeys ( "config-redis" , [ "myconfigkey1" , "myconfigkey2" ], () => {
// Received a key update
});
// When you are ready to stop listening, call the following
await stream . close ();
}
start (). catch (( e ) => {
console . error ( e );
process . exit ( 1 );
});
相关链接 4.3 - JavaScript SDK for Actors 如何使用 Dapr JavaScript SDK 快速上手 Actor
Dapr Actor 包允许你从 JavaScript 应用程序与 Dapr 虚拟 Actor 交互。下面的示例演示了如何使用 JavaScript SDK 与虚拟 Actor 进行交互。
有关 Dapr Actor 的更深入概述,请访问 Actor 概述页面 。
前置条件 场景 下面的代码示例大致描述了停车场车位监控系统的场景,你可以在 Mark Russinovich 的这个视频 中看到。
一个停车场包含数百个停车位,每个停车位都有一个传感器,向中央监控系统提供更新。停车位传感器(我们的 Actor)检测停车位是被占用还是可用。
要立即亲自运行此示例,请克隆源代码,可以在 JavaScript SDK 示例目录 中找到。
Actor 接口 Actor 接口定义了 Actor 实现和调用 Actor 的客户端之间共享的合约。在下面的示例中,我们为停车场传感器创建了一个接口。每个传感器有 2 个方法:carEnter 和 carLeave,它们定义了停车位的状态:
export default interface ParkingSensorInterface {
carEnter () : Promise < void >;
carLeave () : Promise < void >;
}
Actor 实现 Actor 实现通过扩展基类型 AbstractActor 并实现 Actor 接口(在此例中为 ParkingSensorInterface)来定义一个类。
以下代码描述了一个 Actor 实现以及一些辅助方法。
import { AbstractActor } from "@dapr/dapr" ;
import ParkingSensorInterface from "./ParkingSensorInterface" ;
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
async carEnter () : Promise < void > {
// 实现更新该停车位被占用的状态。
}
async carLeave () : Promise < void > {
// 实现更新该停车位可用的状态。
}
private async getInfo () : Promise < object > {
// 实现从停车位传感器请求更新。
}
/**
* @override
*/
async onActivate () : Promise < void > {
// 由 AbstractActor 调用的初始化逻辑。
}
}
配置 Actor 运行时 要配置 Actor 运行时,请使用 DaprClientOptions。各种参数及其默认值记录在 操作指南:在 Dapr 中使用虚拟 Actor 中。
注意,超时和间隔应格式化为 time.ParseDuration 字符串。
import { CommunicationProtocolEnum , DaprClient , DaprServer } from "@dapr/dapr" ;
// 使用 DaprClientOptions 配置 Actor 运行时。
const clientOptions = {
daprHost : daprHost ,
daprPort : daprPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP ,
actor : {
actorIdleTimeout : "1h" ,
actorScanInterval : "30s" ,
drainOngoingCallTimeout : "1m" ,
drainRebalancedActors : true ,
reentrancy : {
enabled : true ,
maxStackDepth : 32 ,
},
remindersStoragePartitions : 0 ,
},
};
// 在创建 DaprServer 和 DaprClient 时使用这些选项。
// 注意,DaprServer 在内部创建一个 DaprClient,需要使用 clientOptions 进行配置。
const server = new DaprServer ({ serverHost , serverPort , clientOptions });
const client = new DaprClient ( clientOptions );
注册 Actor 使用 DaprServer 包初始化并注册你的 Actor:
import { DaprServer } from "@dapr/dapr" ;
import ParkingSensorImpl from "./ParkingSensorImpl" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50000" ;
const serverHost = "127.0.0.1" ;
const serverPort = "50001" ;
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
},
});
await server . actor . init (); // 让服务器知道我们需要 Actor
server . actor . registerActor ( ParkingSensorImpl ); // 注册 Actor
await server . start (); // 启动服务器
// 要获取已注册的 Actor,你可以调用 `getRegisteredActors`:
const resRegisteredActors = await server . actor . getRegisteredActors ();
console . log ( `Registered Actors: ${ JSON . stringify ( resRegisteredActors ) } ` );
调用 Actor 方法 注册 Actor 后,使用 ActorProxyBuilder 创建一个实现 ParkingSensorInterface 的 Proxy 对象。你可以通过直接调用 Proxy 对象上的方法来调用 Actor 方法。在内部,它会转换为对 Actor API 的网络调用并获取结果。
import { ActorId , DaprClient } from "@dapr/dapr" ;
import ParkingSensorImpl from "./ParkingSensorImpl" ;
import ParkingSensorInterface from "./ParkingSensorInterface" ;
const daprHost = "127.0.0.1" ;
const daprPort = "50000" ;
const client = new DaprClient ({ daprHost , daprPort });
// 创建一个新的 Actor 构建器。它可用于创建多个同类型的 Actor。
const builder = new ActorProxyBuilder < ParkingSensorInterface >( ParkingSensorImpl , client );
// 创建一个新的 Actor 实例。
const actor = builder . build ( new ActorId ( "my-actor" ));
// 或者,使用随机 ID
// const actor = builder.build(ActorId.createRandomId());
// 调用方法。
await actor . carEnter ();
在 Actor 中使用状态 import { AbstractActor } from "@dapr/dapr" ;
import ActorStateInterface from "./ActorStateInterface" ;
export default class ActorStateExample extends AbstractActor implements ActorStateInterface {
async setState ( key : string , value : any ) : Promise < void > {
await this . getStateManager (). setState ( key , value );
await this . getStateManager (). saveState ();
}
async removeState ( key : string ) : Promise < void > {
await this . getStateManager (). removeState ( key );
await this . getStateManager (). saveState ();
}
// 使用特定类型的 getState
async getState < T >( key : string ) : Promise < T | null > {
return await this . getStateManager < T >(). getState ( key );
}
// 不使用类型作为 `any` 的 getState
async getState ( key : string ) : Promise < any > {
return await this . getStateManager (). getState ( key );
}
}
Actor 定时器和提醒 JS SDK 支持通过注册定时器或提醒来安排对自己的周期性工作的 Actor。定时器和提醒之间的主要区别在于,Dapr Actor 运行时在停用后不保留有关定时器的任何信息,但使用 Dapr Actor 状态提供程序持久化提醒信息。
这种区别允许用户在轻量级但无状态的定时器和资源密集但有状态的提醒之间进行权衡。
定时器和提醒的调度接口是相同的。有关调度配置的更深入信息,请参阅 Actor 定时器和提醒文档 。
Actor 定时器 // ...
const actor = builder . build ( new ActorId ( "my-actor" ));
// 注册定时器
await actor . registerActorTimer (
"timer-id" , // 定时器的唯一名称。
"cb-method" , // 定时器触发时要执行的回调方法。
Temporal . Duration . from ({ seconds : 2 }), // DueTime
Temporal . Duration . from ({ seconds : 1 }), // Period
Temporal . Duration . from ({ seconds : 1 }), // TTL
50 , // 要发送到定时器回调的状态。
);
// 删除定时器
await actor . unregisterActorTimer ( "timer-id" );
Actor 提醒 // ...
const actor = builder . build ( new ActorId ( "my-actor" ));
// 注册提醒,它有一个默认回调:`receiveReminder`
await actor . registerActorReminder (
"reminder-id" , // 提醒的唯一名称。
Temporal . Duration . from ({ seconds : 2 }), // DueTime
Temporal . Duration . from ({ seconds : 1 }), // Period
Temporal . Duration . from ({ seconds : 1 }), // TTL
100 , // 要发送到提醒回调的状态。
);
// 删除提醒
await actor . unregisterActorReminder ( "reminder-id" );
要处理回调,你需要在你的 Actor 中覆盖默认的 receiveReminder 实现。例如,从我们最初的 Actor 实现:
export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
// ...
/**
* @override
*/
async receiveReminder ( state : any ) : Promise < void > {
// 在这里处理逻辑
}
// ...
}
有关 Actor 的完整指南,请访问 操作指南:在 Dapr 中使用虚拟 Actor 。
4.4 - JavaScript SDK 中的日志记录 在 JavaScript SDK 中配置日志记录
简介 JavaScript SDK 内置了基于 Console 的日志记录器。SDK 会发出各种内部日志,以帮助用户了解事件链并排查问题。SDK 的使用者可以自定义日志的详细程度,也可以提供自己的日志记录器实现。
配置日志级别 日志共有五个级别,按重要性降序排列 - error、warn、info、verbose 和 debug。将日志设置到某个级别意味着日志记录器将发出所有重要性不低于该级别的日志。例如,设置为 verbose 级别意味着 SDK 将不会发出 debug 级别的日志。默认日志级别为 info。
Dapr Client import { CommunicationProtocolEnum , DaprClient , LogLevel } from "@dapr/dapr" ;
// create a client instance with log level set to verbose.
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum . HTTP ,
logger : { level : LogLevel . Verbose },
});
有关如何使用 Client 的更多详细信息,请参阅 JavaScript Client 。
DaprServer import { CommunicationProtocolEnum , DaprServer , LogLevel } from "@dapr/dapr" ;
// create a server instance with log level set to error.
const server = new DaprServer ({
serverHost ,
serverPort ,
clientOptions : {
daprHost ,
daprPort ,
logger : { level : LogLevel.Error },
},
});
有关如何使用 Server 的更多详细信息,请参阅 JavaScript Server 。
自定义 LoggerService JavaScript SDK 使用内置的 Console 进行日志记录。要使用 Winston 或 Pino 等自定义日志记录器,可以实现 LoggerService 接口。
基于 Winston 的日志记录: 创建 LoggerService 的新实现。
import { LoggerService } from "@dapr/dapr" ;
import * as winston from "winston" ;
export class WinstonLoggerService implements LoggerService {
private logger ;
constructor () {
this . logger = winston . createLogger ({
transports : [ new winston . transports . Console (), new winston . transports . File ({ filename : "combined.log" })],
});
}
error ( message : any , ... optionalParams : any []) : void {
this . logger . error ( message , ... optionalParams );
}
warn ( message : any , ... optionalParams : any []) : void {
this . logger . warn ( message , ... optionalParams );
}
info ( message : any , ... optionalParams : any []) : void {
this . logger . info ( message , ... optionalParams );
}
verbose ( message : any , ... optionalParams : any []) : void {
this . logger . verbose ( message , ... optionalParams );
}
debug ( message : any , ... optionalParams : any []) : void {
this . logger . debug ( message , ... optionalParams );
}
}
将新实现传递给 SDK。
import { CommunicationProtocolEnum , DaprClient , LogLevel } from "@dapr/dapr" ;
import { WinstonLoggerService } from "./WinstonLoggerService" ;
const winstonLoggerService = new WinstonLoggerService ();
// create a client instance with log level set to verbose and logger service as winston.
const client = new DaprClient ({
daprHost ,
daprPort ,
communicationProtocol : CommunicationProtocolEnum.HTTP ,
logger : { level : LogLevel.Verbose , service : winstonLoggerService },
});
4.5 - JavaScript 示例 通过示例快速上手 Dapr JavaScript SDK!
快速入门 文章 想要添加您的文章?告诉我们! ,以便我们将其添加到下方
4.6 - 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流 如何使用 Dapr JavaScript SDK 快速上手工作流
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例 ,你将:
本示例使用了在自托管模式 下通过 dapr init 获得的默认配置。
前提条件 设置环境 克隆 JavaScript SDK 仓库并进入该目录。
git clone https://github.com/dapr/js-sdk
cd js-sdk
从 JavaScript SDK 根目录进入 Dapr Workflow 示例。
cd examples/workflow/authoring
运行以下命令以安装使用 Dapr JavaScript SDK 运行此工作流示例所需的所有依赖。
运行 activity-sequence.ts activity-sequence 文件向 Dapr Workflow 运行时注册了一个工作流和一个 activity。该工作流是按顺序执行的多个 activity 组成的序列。我们使用 DaprWorkflowClient 调度新的工作流实例并等待其完成。
const daprHost = "localhost" ;
const daprPort = "50001" ;
const workflowClient = new DaprWorkflowClient ({
daprHost ,
daprPort ,
});
const workflowRuntime = new WorkflowRuntime ({
daprHost ,
daprPort ,
});
const hello = async ( _ : WorkflowActivityContext , name : string ) => {
return `Hello ${ name } !` ;
};
const sequence : TWorkflow = async function * ( ctx : WorkflowContext ) : any {
const cities : string [] = [];
const result1 = yield ctx . callActivity ( hello , "Tokyo" );
cities . push ( result1 );
const result2 = yield ctx . callActivity ( hello , "Seattle" );
cities . push ( result2 );
const result3 = yield ctx . callActivity ( hello , "London" );
cities . push ( result3 );
return cities ;
};
workflowRuntime . registerWorkflow ( sequence ). registerActivity ( hello );
// 将 worker 启动包装在 try-catch 块中以处理启动期间的任何错误
try {
await workflowRuntime . start ();
console . log ( "Workflow runtime started successfully" );
} catch ( error ) {
console . error ( "Error starting workflow runtime:" , error );
}
// 调度新的编排
try {
const id = await workflowClient . scheduleNewWorkflow ( sequence );
console . log ( `Orchestration scheduled with ID: ${ id } ` );
// 等待编排完成
const state = await workflowClient . waitForWorkflowCompletion ( id , undefined , 30 );
console . log ( `Orchestration completed! Result: ${ state ? . serializedOutput } ` );
} catch ( error ) {
console . error ( "Error scheduling or waiting for orchestration:" , error );
}
在以上代码中:
workflowRuntime.registerWorkflow(sequence) 将 sequence 注册为 Dapr Workflow 运行时中的一个工作流。await workflowRuntime.start(); 在 Dapr Workflow 运行时中构建并启动引擎。await workflowClient.scheduleNewWorkflow(sequence) 向 Dapr Workflow 运行时调度一个新的工作流实例。await workflowClient.waitForWorkflowCompletion(id, undefined, 30) 等待工作流实例完成。在终端中执行以下命令以启动 activity-sequence.ts:
npm run start:dapr:activity-sequence
预期输出
You're up and running! Both Dapr and your app logs will appear here.
...
== APP == Orchestration scheduled with ID: dc040bea-6436-4051-9166-c9294f9d2201
== APP == Waiting 30 seconds for instance dc040bea-6436-4051-9166-c9294f9d2201 to complete...
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 0 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello Tokyo!" (14 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 3 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello Seattle!" (16 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 6 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Waiting for 1 task(s) and 0 event(s) to complete...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
== APP == Received "Activity Request" work item
== APP == Activity hello completed with output "Hello London!" (15 chars)
== APP == Received "Orchestrator Request" work item with instance id 'dc040bea-6436-4051-9166-c9294f9d2201'
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Rebuilding local state with 9 history event...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Orchestration completed with status COMPLETED
== APP == dc040bea-6436-4051-9166-c9294f9d2201: Returning 1 action(s)
INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' completed with a COMPLETED status. app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3
== APP == Instance dc040bea-6436-4051-9166-c9294f9d2201 completed
== APP == Orchestration completed! Result: ["Hello Tokyo!","Hello Seattle!","Hello London!"]
后续步骤 5 - Dapr PHP SDK 用于开发 Dapr 应用程序的 PHP SDK 包
Dapr 提供了一个 SDK 来帮助开发 PHP 应用程序。使用它,你可以用 Dapr 创建 PHP 客户端、服务器和虚拟 Actor。
设置 前提条件 可选前提条件 初始化项目 在你想要创建服务的目录中,运行 composer init 并回答问题。
使用 composer require dapr/php-sdk 以及你可能希望使用的任何其他依赖项进行安装。
配置服务 创建一个 config.php,复制以下内容:
<? php
use Dapr\Actors\Generators\ProxyFactory ;
use Dapr\Middleware\Defaults\ { Response\ApplicationJson , Tracing };
use Psr\Log\LogLevel ;
use function DI\ { env , get };
return [
// 设置日志级别
'dapr.log.level' => LogLevel :: WARNING ,
// 在每个请求上生成一个新的代理 - 建议在开发中使用
'dapr.actors.proxy.generation' => ProxyFactory :: GENERATED ,
// 在此处添加任何订阅
'dapr.subscriptions' => [],
// 如果此服务将托管任何 Actor,请在此添加它们
'dapr.actors' => [],
// 如果此服务将托管任何 Actor,配置 Dapr 应将 Actor 视为空闲的时间
'dapr.actors.idle_timeout' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 检查空闲 Actor 的频率
'dapr.actors.scan_interval' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 在排空期间等待 Actor 完成的时间
'dapr.actors.drain_timeout' => null ,
// 如果此服务将托管任何 Actor,配置 Dapr 是否应等待 Actor 完成
'dapr.actors.drain_enabled' => null ,
// 你通常不需要更改此项,但如果需要可以在此设置
'dapr.port' => env ( 'DAPR_HTTP_PORT' , '3500' ),
// 在此处添加任何自定义序列化例程
'dapr.serializers.custom' => [],
// 在此处添加任何自定义反序列化例程
'dapr.deserializers.custom' => [],
// 以下内容无效,因为它是默认中间件并按指定顺序处理
'dapr.http.middleware.request' => [ get ( Tracing :: class )],
'dapr.http.middleware.response' => [ get ( ApplicationJson :: class ), get ( Tracing :: class )],
];
创建服务 创建 index.php 并放入以下内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
use Dapr\App ;
$app = App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( __DIR__ . '/config.php' ));
$app -> get ( '/hello/{name}' , function ( string $name ) {
return [ 'hello' => $name ];
});
$app -> start ();
试用 使用 dapr init 初始化 dapr,然后使用 dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000 启动项目。
现在你可以打开 Web 浏览器并访问 http://localhost:3000/hello/world
将 world 替换为你的名字、宠物的名字或任何你想要的内容。
恭喜,你已经创建了你的第一个 Dapr 服务!我很期待看到你用它做什么!
更多信息 5.1 - 虚拟 Actor 如何构建 Actor
如果你不熟悉 Actor 模式,了解 Actor 模式的最佳位置是
Actor 概述。
在 PHP SDK 中,Actor 有两个方面,即客户端和 Actor(也称为运行时)。作为 Actor 的客户端,
你将通过 ActorProxy 类与远程 Actor 交互。该类使用若干配置策略之一动态地生成代理类。
在编写 Actor 时,状态可以为你管理。你可以钩入 Actor 生命周期,并定义提醒和定时器。
这为你处理适合 Actor 模式的各类问题提供了相当大的能力。
Actor 代理 每当您需要与 Actor 通信时,都需要获取一个代理对象来执行此操作。代理负责
序列化您的请求、反序列化响应并将其返回给您,同时遵守指定接口定义的契约。
为了创建代理,首先需要一个接口来定义您与 Actor 发送和接收的内容和方式。
例如,如果您想与一个仅跟踪计数的计数 Actor 通信,您可以将接口
定义如下:
<? php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
function increment ( int $amount = 1 ) : void ;
function get_count () : int ;
}
将此接口放在 Actor 和客户端都可以访问的共享库中(如果两者都用 PHP 编写),这是一个好主意。DaprType
属性告诉 DaprClient 要发送到的 Actor 名称。它应该与实现的 DaprType 匹配,尽管
如果需要,您可以覆盖该类型。
<? php
$app -> run ( function ( \Dapr\Actors\ActorProxy $actorProxy ) {
$actor = $actorProxy -> get ( ICount :: class , 'actor-id' );
$actor -> increment ( 10 );
});
编写 Actor 要创建 Actor,您需要实现之前定义的接口,并添加 DaprType 属性。所有
Actor 必须 实现 IActor,不过有一个 Actor 基类实现了样板代码,使您的实现
更加简单。
以下是计数 Actor:
<? php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
function __construct ( string $id , private CountState $state ) {
parent :: __construct ( $id );
}
function increment ( int $amount = 1 ) : void {
$this -> state -> count += $amount ;
}
function get_count () : int {
return $this -> state -> count ;
}
}
最重要的是构造函数。它至少接受一个名为 id 的参数,该参数是 Actor 的 id。
任何其他参数都由 DI 容器注入,包括您想要使用的任何 ActorState。
Actor 生命周期 Actor 通过构造函数在每个针对该 Actor 类型的请求上进行实例化。您可以使用它来计算
临时状态或处理您所需的任何特定于请求的启动,例如设置其他客户端或
连接。
实例化 Actor 后,可能会调用 on_activation() 方法。on_activation() 方法在 Actor “唤醒”
或首次创建时调用。它不会在每次请求时调用。
接下来,调用 Actor 方法。这可能来自定时器、提醒或客户端。您可以执行任何需要
完成的工作和/或抛出异常。
最后,工作结果返回给调用者。一段时间后(取决于您如何配置
服务),Actor 将被停用,并将调用 on_deactivation()。如果主机宕机、
daprd 崩溃或发生其他一些阻止其成功调用的错误,则可能不会调用此方法。
Actor 状态 Actor 状态是扩展 ActorState 的"普通旧 PHP 对象"(POPO)。ActorState 基类提供了一些
有用的方法。以下是示例实现:
<? php
class CountState extends \Dapr\Actors\ActorState {
public int $count = 0 ;
}
注册 Actor Dapr 期望在启动时知道服务可以托管哪些 Actor。您需要将其添加到配置中:
如果你想利用预编译的依赖注入,你需要使用一个工厂:
<? php
// in config.php
return [
'dapr.actors' => fn () => [ Counter :: class ],
];
启动应用程序所需的全部内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create (
configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ) -> enableCompilation ( __DIR__ )
);
$app -> start ();
<? php
// in config.php
return [
'dapr.actors' => [ Counter :: class ]
];
启动应用程序所需的全部内容:
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
$app -> start ();
5.1.1 - 生产环境参考:Actor 在生产环境中运行 PHP Actor
代理模式 Actor 代理有四种不同的处理模式。每种模式呈现不同的权衡,你需要在开发和生产环境中进行权衡。
<? php
\Dapr\Actors\Generators\ProxyFactory :: GENERATED ;
\Dapr\Actors\Generators\ProxyFactory :: GENERATED_CACHED ;
\Dapr\Actors\Generators\ProxyFactory :: ONLY_EXISTING ;
\Dapr\Actors\Generators\ProxyFactory :: DYNAMIC ;
可以通过 dapr.actors.proxy.generation 配置键进行设置。
GENERATED
GENERATED_CACHED
ONLY_EXISTING
DYNAMIC 这是默认模式。在此模式下,每个请求都会生成一个类并通过 eval 执行。它主要用于开发,不应在生产环境中使用。
这与 ProxyModes::GENERATED 相同,只是类存储在临时文件中,因此不需要在每次请求时重新生成。它不知道何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时提供此选项。
在此模式下,如果代理类不存在,将抛出异常。当你不想在生产环境中生成代码时,这很有用。你需要确保生成并预加载/自动加载该类。
生成代理 你可以创建一个 composer 脚本按需生成代理,以利用 ONLY_EXISTING 模式。
创建一个 ProxyCompiler.php
<? php
class ProxyCompiler {
private const PROXIES = [
MyActorInterface :: class ,
MyOtherActorInterface :: class ,
];
private const PROXY_LOCATION = __DIR__ . '/proxies/' ;
public static function compile () {
try {
$app = \Dapr\App :: create ();
foreach ( self :: PROXIES as $interface ) {
$output = $app -> run ( function ( \DI\FactoryInterface $factory ) use ( $interface ) {
return \Dapr\Actors\Generators\FileGenerator :: generate ( $interface , $factory );
});
$reflection = new ReflectionClass ( $interface );
$dapr_type = $reflection -> getAttributes ( \Dapr\Actors\Attributes\DaprType :: class )[ 0 ] -> newInstance () -> type ;
$filename = 'dapr_proxy_' . $dapr_type . '.php' ;
file_put_contents ( self :: PROXY_LOCATION . $filename , $output );
echo "Compiled: $interface " ;
}
} catch ( Exception $ex ) {
echo "Failed to generate proxy for $interface\n{ $ex -> getMessage () } on line { $ex -> getLine () } in { $ex -> getFile () } \n " ;
}
}
}
然后为生成的代理添加一个 psr-4 自动加载器,并在 composer.json 中添加一个脚本:
{
"autoload" : {
"psr-4" : {
"Dapr\\Proxies\\" : "path/to/proxies"
}
},
"scripts" : {
"compile-proxies" : "ProxyCompiler::compile"
}
}
最后,配置 dapr 仅使用生成的代理:
<? php
// in config.php
return [
'dapr.actors.proxy.generation' => ProxyFactory :: ONLY_EXISTING ,
];
在此模式下,代理满足接口约定,但实际上并不实现接口本身(意味着 instanceof 将返回 false)。此模式利用 PHP 的一些特性来工作,适用于代码无法被 eval 或生成的场景。
请求 对于任何模式,创建 actor 代理的开销都非常小。创建 actor 代理对象时不会发起任何请求。
当你在代理对象上调用方法时,只有你实现的方法由你的 actor 实现提供服务。get_id() 在本地处理,而 get_reminder()、delete_reminder() 等由 daprd 处理。
Actor 实现 PHP 中的每个 actor 实现都必须实现 \Dapr\Actors\IActor 并使用 \Dapr\Actors\ActorTrait trait。这允许快速反射并提供一些快捷方式。使用 \Dapr\Actors\Actor 抽象基类会自动为你完成这些,但如果你需要覆盖默认行为,可以通过实现接口并使用该 trait 来实现。
激活和停用 当 actor 激活时,会将一个令牌文件写入临时目录(在 Linux 上默认为 '/tmp/dapr_' + sha256(concat(Dapr type, id)),在 Windows 上为 '%temp%/dapr_' + sha256(concat(Dapr type, id)))。该文件会一直保留,直到 actor 停用或主机关闭。这确保了当 Dapr 在主机上激活 actor 时,on_activation 只被调用一次。
性能 在使用 php-fpm 和 nginx 或 Windows 上的 IIS 的生产环境中,actor 方法调用非常快。虽然 actor 在每个请求上都会被构造,但 actor 状态键仅在需要时加载,而不是在每个请求期间加载。但是,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这个问题,以可用性换取速度。不建议从一开始就这样做,而是作为需要时的优化手段。
状态版本控制 ActorState 对象中的变量名直接对应存储中的键名。这意味着如果你更改变量的类型或名称,可能会遇到错误。为了解决这个问题,你可能需要对状态对象进行版本控制。为此,你需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能如下所示:
<? php
class VersionedState extends \Dapr\Actors\ActorState {
/**
* @var int 存储中状态的当前版本。我们给出当前版本的默认值。
* 但是,它在存储中可能有不同的值。
*/
public int $state_version = self :: VERSION ;
/**
* @var int 数据的当前版本
*/
private const VERSION = 3 ;
/**
* 在你的 actor 激活时调用。
*/
public function upgrade () {
if ( $this -> state_version < self :: VERSION ) {
$value = parent :: __get ( $this -> get_versioned_key ( 'key' , $this -> state_version ));
// 更新数据结构后更新值
parent :: __set ( $this -> get_versioned_key ( 'key' , self :: VERSION ), $value );
$this -> state_version = self :: VERSION ;
$this -> save_state ();
}
}
// 如果你在上面的方法中根据需要升级所有键,则不需要在加载/保存时遍历以前的
// 键,你只需获取键的当前版本。
private function get_previous_version ( int $version ) : int {
return $this -> has_previous_version ( $version ) ? $version - 1 : $version ;
}
private function has_previous_version ( int $version ) : bool {
return $version >= 0 ;
}
private function walk_versions ( int $version , callable $callback , callable $predicate ) : mixed {
$value = $callback ( $version );
if ( $predicate ( $value ) || ! $this -> has_previous_version ( $version )) {
return $value ;
}
return $this -> walk_versions ( $this -> get_previous_version ( $version ), $callback , $predicate );
}
private function get_versioned_key ( string $key , int $version ) {
return $this -> has_previous_version ( $version ) ? $version . $key : $key ;
}
public function __get ( string $key ) : mixed {
return $this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __get ( $this -> get_versioned_key ( $key , $version )),
fn ( $value ) => isset ( $value )
);
}
public function __isset ( string $key ) : bool {
return $this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __isset ( $this -> get_versioned_key ( $key , $version )),
fn ( $isset ) => $isset
);
}
public function __set ( string $key , mixed $value ) : void {
// 可选:你可以取消设置键的以前版本
parent :: __set ( $this -> get_versioned_key ( $key , self :: VERSION ), $value );
}
public function __unset ( string $key ) : void {
// 取消设置此版本和所有以前版本
$this -> walk_versions (
self :: VERSION ,
fn ( $version ) => parent :: __unset ( $this -> get_versioned_key ( $key , $version )),
fn () => false
);
}
}
有很多可以优化的地方,直接在生产环境中使用它并不是一个好主意,但你可以了解它的工作原理。其中大部分将取决于你的用例,这也是 SDK 中没有类似功能的原因。例如,在此示例实现中,保留以前的值是为了防止升级期间可能出现的错误;保留以前的值允许再次运行升级,但你可能希望删除以前的值。
5.2 - 使用 PHP 进行发布订阅 如何使用
使用 Dapr,你可以发布任何内容,包括云事件。SDK 包含一个简单的云事件实现,但你也可以直接传递一个符合云事件规范的数组,或者使用其他库。
<? php
$app -> post ( '/publish' , function ( \Dapr\Client\DaprClient $daprClient ) {
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : [ 'something' => 'happened' ]);
});
有关发布/订阅的更多信息,请查看操作指南 。
数据内容类型 PHP SDK 允许在构造自定义云事件或发布原始数据时设置数据内容类型。
<? php
$event = new \Dapr\PubSub\CloudEvent ();
$event -> data = $xml ;
$event -> data_content_type = 'application/xml' ;
<? php
/**
* @var \Dapr\Client\DaprClient $daprClient
*/
$daprClient -> publishEvent ( pubsubName : 'pubsub' , topicName : 'my-topic' , data : $raw_data , contentType : 'application/octet-stream' );
Binary data 二进制数据仅支持 <code>application/octet-steam</code>。
接收云事件 在你的订阅处理程序中,你可以让 DI 容器向你的控制器中注入 Dapr\PubSub\CloudEvent 或 array。前者会进行一些验证以确保你有一个正确的事件。如果你需要直接访问数据,或者事件不符合规范,请使用 array。
5.3 - 应用 使用 App 类
在 PHP 中,没有默认的路由器。因此,提供了 \Dapr\App 类。它在底层使用
Nikic’s FastRoute 。不过,您可以根据需要自由使用任何路由器或
框架。只需查看 App 类中的 add_dapr_routes() 方法,即可了解 actors 和
订阅是如何实现的。
每个应用都应从 App::create() 开始,它接受两个参数,第一个是现有的 DI 容器(如果
您有的话),第二个是回调函数,用于钩入 ContainerBuilder 并添加您自己的配置。
在此基础上,您应该定义路由,然后调用 $app->start() 来执行当前请求的路由。
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个 GET /test/{id} 的控制器,返回 id
$app -> get ( '/test/{id}' , fn ( string $id ) => $id );
$app -> start ();
从控制器返回 您可以从控制器返回任何内容,它将被序列化为 json 对象。您也可以请求
Psr Response 对象并返回该对象,从而允许您自定义标头,并对整个响应进行控制:
<? php
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions ( 'config.php' ));
// 添加一个 GET /test/{id} 的控制器,返回 id
$app -> get ( '/test/{id}' ,
fn (
string $id ,
\Psr\Http\Message\ResponseInterface $response ,
\Nyholm\Psr7\Factory\Psr17Factory $factory ) => $response -> withBody ( $factory -> createStream ( $id )));
$app -> start ();
将应用作为客户端使用 当您只想将 Dapr 用作客户端时,例如在现有代码中,可以调用 $app->run()。在这些情况下,通常
不需要自定义配置,不过,您可能希望使用编译后的 DI 容器,特别是在生产环境中:
<? php
// app.php
require_once __DIR__ . '/vendor/autoload.php' ;
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> enableCompilation ( __DIR__ ));
$result = $app -> run ( fn ( \Dapr\DaprClient $client ) => $client -> get ( '/invoke/other-app/method/my-method' ));
在其他框架中使用 提供了 DaprClient 对象,实际上,App 对象使用的所有便捷方法都是基于 DaprClient 构建的。
<? php
require_once __DIR__ . '/vendor/autoload.php' ;
$clientBuilder = \Dapr\Client\DaprClient :: clientBuilder ();
// 您可以自定义(反)序列化,或注释掉以使用默认的 JSON 序列化器。
$clientBuilder = $clientBuilder -> withSerializationConfig ( $yourSerializer ) -> withDeserializationConfig ( $yourDeserializer );
// 您还可以传入一个 logger
$clientBuilder = $clientBuilder -> withLogger ( $myLogger );
// 以及更改边车的 url,例如,使用 https
$clientBuilder = $clientBuilder -> useHttpClient ( 'https://localhost:3800' )
在调用之前,您可以调用多个函数
5.3.1 - 单元测试 单元测试
单元测试和集成测试是 PHP SDK 的一等公民。通过使用 DI 容器、mock、stub 以及提供的 \Dapr\Mocks\TestClient,你可以编写非常细粒度的测试。
测试 Actor 在测试 Actor 时,我们关注两件事:
基于初始状态的返回结果 基于初始状态的最终状态
使用 TestClient 进行集成测试
单元测试 下面是一个测试非常简单的 actor 的示例,该 actor 更新其状态并返回特定值:
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
private \DI\Container $container ;
public function setUp () : void
{
parent :: setUp ();
// 创建一个默认应用并从中提取 DI 容器
$app = \Dapr\App :: create (
configure : fn ( \DI\ContainerBuilder $builder ) => $builder -> addDefinitions (
[ 'dapr.actors' => [ TestActor :: class ]],
[ \Dapr\DaprClient :: class => \DI\autowire ( \Dapr\Mocks\TestClient :: class )]
));
$app -> run ( fn ( \DI\Container $container ) => $this -> container = $container );
}
public function testIncrementsWhenOdd ()
{
$id = uniqid ();
$runtime = $this -> container -> get ( \Dapr\Actors\ActorRuntime :: class );
$client = $this -> getClient ();
// 从 http://localhost:1313/reference/api/actors_api/ 返回当前状态
$client -> register_get ( "/actors/TestActor/ $id /state/number" , code : 200 , data : 3 );
// 确保从 http://localhost:1313/reference/api/actors_api/ 递增
$client -> register_post (
"/actors/TestActor/ $id /state" ,
code : 204 ,
response_data : null ,
expected_request : [
[
'operation' => 'upsert' ,
'request' => [
'key' => 'number' ,
'value' => 4 ,
],
],
]
);
$result = $runtime -> resolve_actor (
'TestActor' ,
$id ,
fn ( $actor ) => $runtime -> do_method ( $actor , 'oddIncrement' , null )
);
$this -> assertTrue ( $result );
}
private function getClient () : \Dapr\Mocks\TestClient
{
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
}
<? php
// TestState.php
class TestState extends \Dapr\Actors\ActorState
{
public int $number ;
}
// TestActor.php
#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
public function __construct ( string $id , private TestState $state )
{
parent :: __construct ( $id );
}
public function oddIncrement () : bool
{
if ( $this -> state -> number % 2 === 0 ) {
return false ;
}
$this -> state -> number += 1 ;
return true ;
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase
{
public function testNotIncrementsWhenEven () {
$container = new \DI\Container ();
$state = new TestState ( $container , $container );
$state -> number = 4 ;
$id = uniqid ();
$actor = new TestActor ( $id , $state );
$this -> assertFalse ( $actor -> oddIncrement ());
$this -> assertSame ( 4 , $state -> number );
}
}
测试事务 在基于事务构建时,你可能需要测试如何处理失败的事务。为此,你需要注入故障并确保事务符合你的预期。
使用 TestClient 进行集成测试
单元测试 <? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
private \DI\Container $container ;
public function setUp () : void
{
parent :: setUp ();
$app = \Dapr\App :: create ( configure : fn ( \DI\ContainerBuilder $builder )
=> $builder -> addDefinitions ([ \Dapr\DaprClient :: class => \DI\autowire ( \Dapr\Mocks\TestClient :: class )]));
$this -> container = $app -> run ( fn ( \DI\Container $container ) => $container );
}
private function getClient () : \Dapr\Mocks\TestClient {
return $this -> container -> get ( \Dapr\DaprClient :: class );
}
public function testTransactionFailure () {
$client = $this -> getClient ();
// 从 https://docs.dapr.io/zh-hans/reference/api/state_api/ 创建响应
$client -> register_post ( '/state/statestore/bulk' , code : 200 , response_data : [
[
'key' => 'value' ,
// 没有先前的值
],
], expected_request : [
'keys' => [ 'value' ],
'parallelism' => 10
]);
$client -> register_post ( '/state/statestore/transaction' ,
code : 200 ,
response_data : null ,
expected_request : [
'operations' => [
[
'operation' => 'upsert' ,
'request' => [
'key' => 'value' ,
'value' => 'hello world'
]
]
]
]
);
$state = new MyState ( $this -> container , $this -> container );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
<? php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
public string $value = '' ;
}
// SomeService.php
class SomeService {
public function __construct ( private MyState $state ) {}
public function doWork () {
$this -> state -> begin ();
$this -> state -> value = "hello world" ;
$this -> state -> commit ();
}
}
// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
public function testTransactionFailure () {
$state = $this -> createStub ( MyState :: class );
$service = new SomeService ( $state );
$service -> doWork ();
$this -> assertSame ( 'hello world' , $state -> value );
}
}
5.4 - 使用 PHP 进行状态管理 如何使用
Dapr 为在应用程序中使用状态提供了一种优秀的模块化方法。学习基础知识的最佳方式是访问
操作指南 。
元数据 许多状态组件允许您向组件传递元数据以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递该元数据:
<? php
// 使用 state manager
$app -> run (
fn ( \Dapr\State\StateManager $stateManager ) =>
$stateManager -> save_state ( 'statestore' , new \Dapr\State\StateItem ( 'key' , 'value' , metadata : [ 'port' => '112' ])));
// 使用 DaprClient
$app -> run ( fn ( \Dapr\Client\DaprClient $daprClient ) => $daprClient -> saveState ( storeName : 'statestore' , key : 'key' , value : 'value' , metadata : [ 'port' => '112' ]))
这是向 Cassandra 传递端口元数据的示例。
每个状态操作都允许传递元数据。
一致性/并发 在 PHP SDK 中,有四个类代表 Dapr 中四种不同类型的一致性和并发:
<? php
[
\Dapr\consistency\StrongLastWrite :: class ,
\Dapr\consistency\StrongFirstWrite :: class ,
\Dapr\consistency\EventualLastWrite :: class ,
\Dapr\consistency\EventualFirstWrite :: class ,
]
将其中之一传递给 StateManager 方法或使用 StateStore() 属性,您可以定义状态存储应如何处理冲突。
并行度 执行批量读取或开始事务时,您可以指定并行度数量。如果 Dapr 必须一次读取一个键,它将从底层存储中"最多"一次读取该数量的键。这有助于控制状态存储上的负载,但会降低性能。默认值为 10。
前缀 硬编码的键名称很有用,但为什么不使状态对象更具可重用性呢?在提交事务或将对象保存到状态时,您可以传递一个应用于对象中每个键的前缀。
Transaction prefix
StateManager prefix <? php
class TransactionObject extends \Dapr\State\TransactionalState {
public string $key ;
}
$app -> run ( function ( TransactionObject $object ) {
$object -> begin ( prefix : 'my-prefix-' );
$object -> key = 'value' ;
// 提交到键 `my-prefix-key`
$object -> commit ();
});
<? php
class StateObject {
public string $key ;
}
$app -> run ( function ( \Dapr\State\StateManager $stateManager ) {
$stateManager -> load_object ( $obj = new StateObject (), prefix : 'my-prefix-' );
// 原始值来自 `my-prefix-key`
$obj -> key = 'value' ;
// 保存到 `my-prefix-key`
$stateManager -> save_object ( $obj , prefix : 'my-prefix-' );
});
5.5 - 自定义序列化 如何配置序列化
Dapr 使用 JSON 序列化,因此在发送/接收数据时会丢失(复杂)类型信息。
序列化 当从控制器返回对象、将对象传递给 DaprClient,或将对象存储在状态存储中时,
只有公共属性会被扫描和序列化。你可以通过实现 \Dapr\Serialization\ISerialize 来自定义此行为。
例如,如果你想创建一个序列化为字符串的 ID 类型,可以像这样实现:
<? php
class MyId implements \Dapr\Serialization\Serializers\ISerialize
{
public string $id ;
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// $value === $this
return $this -> id ;
}
}
这适用于我们拥有完全所有权的任何类型,但它不适用于来自库或 PHP 本身的类。
为此,你需要向 DI 容器注册自定义序列化器:
<? php
// 在 config.php 中
class SerializeSomeClass implements \Dapr\Serialization\Serializers\ISerialize
{
public function serialize ( mixed $value , \Dapr\Serialization\ISerializer $serializer ) : mixed
{
// 序列化 $value 并返回结果
}
}
return [
'dapr.serializers.custom' => [ SomeClass :: class => new SerializeSomeClass ()],
];
反序列化 反序列化的工作方式完全相同,只是接口是 \Dapr\Deserialization\Deserializers\IDeserialize。
6 - Dapr Python SDK 用于开发 Dapr 应用程序的 Python SDK 软件包
Dapr 提供了多种子包来帮助开发 Python 应用程序。使用它们,你可以使用 Dapr 创建 Python 客户端、服务器和虚拟 actor。
前提条件 安装 要开始使用 Python SDK,请安装主要的 Dapr Python SDK 软件包。
注意: 开发包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 dapr-dev 软件包之前,请确保卸载任何稳定版本的 Python SDK。
可用的子包 SDK 导入 Python SDK 导入是随主 SDK 安装包含的子包,但在使用时需要导入。Dapr Python SDK 提供的最常见导入包括:
Client 编写 Python 应用程序以与 Dapr 边车和其他 Dapr 应用程序交互,包括 Python 中的有状态虚拟 actor
Actors 创建并与 Dapr 的 Actor 框架交互。
Conversation 使用 Dapr Conversation API(Alpha)进行 LLM 交互、工具和多轮流程。
了解有关 所有 可用的 Dapr Python SDK 导入 的更多信息。
SDK 扩展 SDK 扩展主要用作接收发布订阅事件、以编程方式创建发布订阅订阅以及处理输入绑定事件的实用工具。虽然你可以在不使用扩展的情况下完成所有这些任务,但使用 Python SDK 扩展会更加方便。
gRPC 使用 gRPC 服务器扩展创建 Dapr 服务。
FastAPI 使用 Dapr FastAPI 扩展与 Dapr Python 虚拟 actor 和发布订阅集成。
Flask 使用 Dapr Flask 扩展与 Dapr Python 虚拟 actor 集成。
Workflow 在 Python 中编写与其他 Dapr API 配合使用的工作流。
了解有关 Dapr Python SDK 扩展 的更多信息。
试用 克隆 Python SDK 仓库。
git clone https://github.com/dapr/python-sdk.git
浏览 Python 快速入门、教程和示例,查看 Dapr 的实际运行情况:
SDK 示例 描述 快速入门 在几分钟内使用 Python SDK 体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以试用一些示例并开始使用。 绑定教程 了解 Dapr Python SDK 如何与其他 Dapr SDK 配合工作以启用绑定。 分布式计算器教程 使用 Dapr Python SDK 处理方法调用和状态持久化功能。 Hello World 教程 了解如何使用 Python SDK 在本地机器上启动和运行 Dapr。 Hello Kubernetes 教程 在 Kubernetes 集群中使用 Dapr Python SDK 启动和运行。 可观测性教程 使用 Python SDK 探索 Dapr 的指标收集、链路追踪、日志记录和健康检查功能。 发布订阅教程 了解 Dapr Python SDK 如何与其他 Dapr SDK 配合工作以启用发布订阅应用程序。
更多信息 序列化 了解有关 Dapr SDK 中序列化的更多信息。
6.1 - Dapr 客户端 Python SDK 入门 如何快速上手使用 Dapr Python SDK
Dapr 客户端包允许你从 Python 应用程序与其他 Dapr 应用程序进行交互。
注意 如果还没有,请尝试其中一个
快速入门 ,快速了解如何将 Dapr Python SDK 与 API 构建块一起使用。
前提条件 在开始之前,安装 Dapr Python 包 。
导入客户端包 dapr 包包含 DaprClient,用于创建和使用客户端。
from dapr.clients import DaprClient
初始化客户端 你可以通过多种方式初始化 Dapr 客户端:
默认值: 当不使用任何参数初始化客户端时,它将使用 Dapr 边车实例的默认值(127.0.0.1:50001)。
from dapr.clients import DaprClient
with DaprClient () as d :
# 使用客户端
初始化时指定端点: 当在构造函数中作为参数传递时,gRPC 端点优先于任何配置或环境变量。
from dapr.clients import DaprClient
with DaprClient ( "mydomain:50051?tls=true" ) as d :
# 使用客户端
配置选项: Dapr 边车端点 你可以使用标准化的 DAPR_GRPC_ENDPOINT 环境变量来指定 gRPC 端点。设置此变量后,可以在不使用任何参数的情况下初始化客户端:
export DAPR_GRPC_ENDPOINT = "mydomain:50051?tls=true"
from dapr.clients import DaprClient
with DaprClient () as d :
# 客户端将使用环境变量中指定的端点
旧的环境变量 DAPR_RUNTIME_HOST、DAPR_HTTP_PORT 和 DAPR_GRPC_PORT 也受支持,但 DAPR_GRPC_ENDPOINT 优先。
Dapr API 令牌 如果你的 Dapr 实例配置为需要 DAPR_API_TOKEN 环境变量,你可以在环境中设置它,客户端将自动使用它。 你可以在这里 阅读更多关于 Dapr API 令牌身份验证的信息。
健康检查超时 在客户端初始化时,会针对 Dapr 边车(/healthz/outbound)执行健康检查。
客户端将等待边车启动并运行后再继续。
默认健康检查超时为 60 秒,但可以通过设置 DAPR_HEALTH_TIMEOUT 环境变量来覆盖。
重试和超时 如果从边车收到特定的错误代码,Dapr 客户端可以重试请求。这可以通过 DAPR_API_MAX_RETRIES 环境变量配置,并且会自动拾取,不需要任何代码更改。
DAPR_API_MAX_RETRIES 的默认值是 0,表示不会进行重试。
你可以通过创建 dapr.clients.retry.RetryPolicy 对象并将其传递给 DaprClient 构造函数来微调更多重试参数:
from dapr.clients.retry import RetryPolicy
retry = RetryPolicy (
max_attempts = 5 ,
initial_backoff = 1 ,
max_backoff = 20 ,
backoff_multiplier = 1.5 ,
retryable_http_status_codes = [ 408 , 429 , 500 , 502 , 503 , 504 ],
retryable_grpc_status_codes = [ StatusCode . UNAVAILABLE , StatusCode . DEADLINE_EXCEEDED , ]
)
with DaprClient ( retry_policy = retry ) as d :
...
或者对于 actors:
factory = ActorProxyFactory ( retry_policy = RetryPolicy ( max_attempts = 3 ))
proxy = ActorProxy . create ( 'DemoActor' , ActorId ( '1' ), DemoActorInterface , factory )
超时 可以通过环境变量 DAPR_API_TIMEOUT_SECONDS 为所有调用设置。默认值为 60 秒。
注意:你可以单独控制服务调用的超时,方法是将 timeout 参数传递给 invoke_method 方法。
错误处理 最初,Dapr 中的错误遵循标准 gRPC 错误模型 。然而,为了提供更详细和更有信息量的错误消息,在 1.13 版本中引入了一个增强的错误模型,该模型与 gRPC 更丰富的错误模型 保持一致。作为响应,Python SDK 实现了 DaprGrpcError,这是一个自定义异常类,旨在改善开发人员体验。 值得注意的是,对于所有 gRPC 状态异常,使用 DaprGrpcError 的转换正在进行中。截至目前,并非 SDK 中的每个 API 调用都已更新以利用此自定义异常。我们正在积极进行此增强,并欢迎社区的贡献。
使用 Dapr python-SDK 时处理 DaprGrpcError 异常的示例:
try :
d . save_state ( store_name = storeName , key = key , value = value )
except DaprGrpcError as err :
print ( f 'Status code: { err . code () } ' )
print ( f "Message: { err . message () } " )
print ( f "Error code: { err . error_code () } " )
print ( f "Error info(reason): { err . error_info . reason } " )
print ( f "Resource info (resource type): { err . resource_info . resource_type } " )
print ( f "Resource info (resource name): { err . resource_info . resource_name } " )
print ( f "Bad request (field): { err . bad_request . field_violations [ 0 ] . field } " )
print ( f "Bad request (description): { err . bad_request . field_violations [ 0 ] . description } " )
构建块 Python SDK 允许你与所有 Dapr 构建块 进行交互。
调用服务 Dapr Python SDK 提供了一个简单的 API,可以通过 HTTP 或 gRPC(已弃用)调用服务。可以通过设置 DAPR_API_METHOD_INVOCATION_PROTOCOL 环境变量来选择协议,未设置时默认为 HTTP。Dapr 中的 GRPC 服务调用已弃用,建议使用 GRPC 代理作为替代。
from dapr.clients import DaprClient
with DaprClient () as d :
# 调用方法(gRPC 或 HTTP GET)
resp = d . invoke_method ( 'service-to-invoke' , 'method-to-invoke' , data = '{"message":"Hello World"}' )
# 对于其他 HTTP 动词,必须指定动词
# 调用 'POST' 方法(仅 HTTP)
resp = d . invoke_method ( 'service-to-invoke' , 'method-to-invoke' , data = '{"id":"100", "FirstName":"Value", "LastName":"Value"}' , http_verb = 'post' )
HTTP api 调用的基本端点在 DAPR_HTTP_ENDPOINT 环境变量中指定。
如果未设置此变量,端点值将从 DAPR_RUNTIME_HOST 和 DAPR_HTTP_PORT 变量派生,其默认值分别为 127.0.0.1 和 3500。
gRPC 调用的基本端点是用于客户端初始化的端点(上文解释 )。
保存和获取应用程序状态 from dapr.clients import DaprClient
with DaprClient () as d :
# 保存状态
d . save_state ( store_name = "statestore" , key = "key1" , value = "value1" )
# 获取状态
data = d . get_state ( store_name = "statestore" , key = "key1" ) . data
# 删除状态
d . delete_state ( store_name = "statestore" , key = "key1" )
查询应用程序状态(Alpha) from dapr import DaprClient
query = '''
{
"filter": {
"EQ": { "state": "CA" }
},
"sort": [
{
"key": "person.id",
"order": "DESC"
}
]
}
'''
with DaprClient () as d :
resp = d . query_state (
store_name = 'state_store' ,
query = query ,
states_metadata = { "metakey" : "metavalue" }, # 可选
)
发布和订阅 发布消息 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . publish_event ( pubsub_name = 'pubsub' , topic_name = 'TOPIC_A' , data = '{"message":"Hello World"}' )
发送带有 json 有效负载的 CloudEvents 消息:
from dapr.clients import DaprClient
import json
with DaprClient () as d :
cloud_event = {
'specversion' : '1.0' ,
'type' : 'com.example.event' ,
'source' : 'my-service' ,
'id' : 'myid' ,
'data' : { 'id' : 1 , 'message' : 'hello world' },
'datacontenttype' : 'application/json' ,
}
# 将数据内容类型设置为 'application/cloudevents+json'
resp = d . publish_event (
pubsub_name = 'pubsub' ,
topic_name = 'TOPIC_CE' ,
data = json . dumps ( cloud_event ),
data_content_type = 'application/cloudevents+json' ,
)
发布带有纯文本有效负载的 CloudEvents 消息:
from dapr.clients import DaprClient
import json
with DaprClient () as d :
cloud_event = {
'specversion' : '1.0' ,
'type' : 'com.example.event' ,
'source' : 'my-service' ,
'id' : "myid" ,
'data' : 'hello world' ,
'datacontenttype' : 'text/plain' ,
}
# 将数据内容类型设置为 'application/cloudevents+json'
resp = d . publish_event (
pubsub_name = 'pubsub' ,
topic_name = 'TOPIC_CE' ,
data = json . dumps ( cloud_event ),
data_content_type = 'application/cloudevents+json' ,
)
订阅消息 from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import json
app = App ()
# 主题的默认订阅
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' )
def mytopic ( event : v1 . Event ) -> None :
data = json . loads ( event . Data ())
print ( f 'Received: id= { data [ "id" ] } , message=" { data [ "message" ] } "'
' content_type=" {event.content_type} "' , flush = True )
# 使用发布/订阅路由的特定处理程序
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' ,
rule = Rule ( "event.type == \" important \" " , 1 ))
def mytopic_important ( event : v1 . Event ) -> None :
data = json . loads ( event . Data ())
print ( f 'Received: id= { data [ "id" ] } , message=" { data [ "message" ] } "'
' content_type=" {event.content_type} "' , flush = True )
流式消息订阅 你可以使用 subscribe 或 subscribe_handler 方法创建对 PubSub 主题的流式订阅。
subscribe 方法返回一个可迭代的 Subscription 对象,允许你使用 for 循环(例如 for message in subscription)或调用 next_message 方法从流中拉取消息。这将在等待消息时阻塞主线程。
完成后,你应该调用 close 方法来终止订阅并停止接收消息。
subscribe_with_handler 方法接受一个回调函数,该函数对从流中接收到的每条消息执行。
它在单独的线程中运行,因此不会阻塞主线程。回调应返回一个 TopicEventResponse(例如 TopicEventResponse('success')),指示消息是成功处理、应该重试还是应该丢弃。该方法将根据返回的状态自动管理消息确认。对 subscribe_with_handler 方法的调用返回一个关闭函数,完成后应调用该函数以终止订阅。
以下是使用 subscribe 方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError , StreamCancelledError
counter = 0
def process_message ( message ):
global counter
counter += 1
# 在此处处理消息
print ( f 'Processing message: { message . data () } from { message . topic () } ...' )
return 'success'
def main ():
with DaprClient () as client :
global counter
subscription = client . subscribe (
pubsub_name = 'pubsub' , topic = 'TOPIC_A' , dead_letter_topic = 'TOPIC_A_DEAD'
)
try :
for message in subscription :
if message is None :
print ( 'No message received. The stream might have been cancelled.' )
continue
try :
response_status = process_message ( message )
if response_status == 'success' :
subscription . respond_success ( message )
elif response_status == 'retry' :
subscription . respond_retry ( message )
elif response_status == 'drop' :
subscription . respond_drop ( message )
if counter >= 5 :
break
except StreamInactiveError :
print ( 'Stream is inactive. Retrying...' )
time . sleep ( 1 )
continue
except StreamCancelledError :
print ( 'Stream was cancelled' )
break
except Exception as e :
print ( f 'Error occurred during message processing: { e } ' )
finally :
print ( 'Closing subscription...' )
subscription . close ()
if __name__ == '__main__' :
main ()
以下是使用 subscribe_with_handler 方法的示例:
import time
from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse
counter = 0
def process_message ( message ):
# 在此处处理消息
global counter
counter += 1
print ( f 'Processing message: { message . data () } from { message . topic () } ...' )
return TopicEventResponse ( 'success' )
def main ():
with ( DaprClient () as client ):
# 这将启动一个新线程来监听消息
# 并在 `process_message` 函数中处理它们
close_fn = client . subscribe_with_handler (
pubsub_name = 'pubsub' , topic = 'TOPIC_A' , handler_fn = process_message ,
dead_letter_topic = 'TOPIC_A_DEAD'
)
while counter < 5 :
time . sleep ( 1 )
print ( "Closing subscription..." )
close_fn ()
if __name__ == '__main__' :
main ()
对话(Alpha)
注意 Dapr 对话 API 目前处于 alpha 阶段。从 1.15 版本开始,Dapr 为开发者提供了通过对话 API 与大型语言模型(LLM)安全可靠地交互的能力。
from dapr.clients import DaprClient
from dapr.clients.grpc.conversation import ConversationInput
with DaprClient () as d :
inputs = [
ConversationInput ( content = "What's Dapr?" , role = 'user' , scrub_pii = True ),
ConversationInput ( content = 'Give a brief overview.' , role = 'user' , scrub_pii = True ),
]
metadata = {
'model' : 'foo' ,
'key' : 'authKey' ,
'cacheTTL' : '10m' ,
}
response = d . converse_alpha1 (
name = 'echo' , inputs = inputs , temperature = 0.7 , context_id = 'chat-123' , metadata = metadata
)
for output in response . outputs :
print ( f 'Result: { output . result } ' )
与输出绑定交互 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . invoke_binding ( binding_name = 'kafkaBinding' , operation = 'create' , data = '{"message":"Hello World"}' )
检索密钥 from dapr.clients import DaprClient
with DaprClient () as d :
resp = d . get_secret ( store_name = 'localsecretstore' , key = 'secretKey' )
配置 获取配置 from dapr.clients import DaprClient
with DaprClient () as d :
# 获取配置
configuration = d . get_configuration ( store_name = 'configurationstore' , keys = [ 'orderId' ], config_metadata = {})
订阅配置 import asyncio
from time import sleep
from dapr.clients import DaprClient
async def executeConfiguration ():
with DaprClient () as d :
storeName = 'configurationstore'
key = 'orderId'
# 等待边车在 20 秒内启动。
d . wait ( 20 )
# 按键订阅配置。
configuration = await d . subscribe_configuration ( store_name = storeName , keys = [ key ], config_metadata = {})
while True :
if configuration != None :
items = configuration . get_items ()
for key , item in items :
print ( f "Subscribe key= { key } value= { item . value } version= { item . version } " , flush = True )
else :
print ( "Nothing yet" )
sleep ( 5 )
asyncio . run ( executeConfiguration ())
分布式锁 from dapr.clients import DaprClient
def main ():
# 锁参数
store_name = 'lockstore' # 在 components/lockstore.yaml 中定义
resource_id = 'example-lock-resource'
client_id = 'example-client-id'
expiry_in_seconds = 60
with DaprClient () as dapr :
print ( 'Will try to acquire a lock from lock store named [ %s ]' % store_name )
print ( 'The lock is for a resource named [ %s ]' % resource_id )
print ( 'The client identifier is [ %s ]' % client_id )
print ( 'The lock will will expire in %s seconds.' % expiry_in_seconds )
with dapr . try_lock ( store_name , resource_id , client_id , expiry_in_seconds ) as lock_result :
assert lock_result . success , 'Failed to acquire the lock. Aborting.'
print ( 'Lock acquired successfully!!!' )
# 此时锁已释放 - 通过 `with` 子句的魔法 ;)
unlock_result = dapr . unlock ( store_name , resource_id , client_id )
print ( 'We already released the lock so unlocking will not work.' )
print ( 'We tried to unlock it anyway and got back [ %s ]' % unlock_result . status )
加密 from dapr.clients import DaprClient
message = 'The secret is "passw0rd"'
def main ():
with DaprClient () as d :
resp = d . encrypt (
data = message . encode (),
options = EncryptOptions (
component_name = 'crypto-localstorage' ,
key_name = 'rsa-private-key.pem' ,
key_wrap_algorithm = 'RSA' ,
),
)
encrypt_bytes = resp . read ()
resp = d . decrypt (
data = encrypt_bytes ,
options = DecryptOptions (
component_name = 'crypto-localstorage' ,
key_name = 'rsa-private-key.pem' ,
),
)
decrypt_bytes = resp . read ()
print ( decrypt_bytes . decode ()) # The secret is "passw0rd"
相关链接 Python SDK 示例
6.2 - Dapr Actor Python SDK 入门 如何快速上手使用 Dapr Python SDK
Dapr actor 包使您能够从 Python 应用程序与 Dapr virtual actors 交互。
前置条件 Actor 接口 接口定义了 actor 实现与调用 actor 的客户端之间共享的 actor 契约。由于客户端可能依赖该接口,因此通常将其定义在与 actor 实现分离的程序集中。
from dapr.actor import ActorInterface , actormethod
class DemoActorInterface ( ActorInterface ):
@actormethod ( name = "GetMyData" )
async def get_my_data ( self ) -> object :
...
Actor 服务 actor 服务托管 virtual actor。它由一个派生自基类型 Actor 并实现 actor 接口中定义的接口的类来实现。
可以使用以下 Dapr actor 扩展之一创建 actor:
Actor 客户端 actor 客户端包含 actor 客户端的实现,该实现调用 actor 接口中定义的 actor 方法。
import asyncio
from dapr.actor import ActorProxy , ActorId
from demo_actor_interface import DemoActorInterface
async def main ():
# 创建代理客户端
proxy = ActorProxy . create ( 'DemoActor' , ActorId ( '1' ), DemoActorInterface )
# 在客户端上调用方法
resp = await proxy . GetMyData ()
示例 请访问此页面 查看可运行的 actor 示例。
Mock Actor 测试 Dapr Python SDK 提供了创建 mock actor 的功能,用于对 actor 方法进行单元测试,并查看它们如何与 actor 状态交互。
示例用法 from dapr.actor.runtime.mock_actor import create_mock_actor
class MyActor(Actor, MyActorInterface):
async def save_state(self, data) -> None:
await self._state_manager.set_state('mystate', data)
await self._state_manager.save_state()
mock_actor = create_mock_actor(MyActor, "id")
await mock_actor.save_state(5)
assert mockactor._state_manager._mock_state['mystate'] == 5 #True
Mock actor 通过将 actor 类和 actor ID(字符串)传递给 create_mock_actor 函数来创建。此函数返回一个 actor 实例,其中许多内部方法已被覆盖。Mock actor 不与 Dapr 交互来执行保存状态或管理定时器等任务,而是使用内存状态来模拟这些行为。
可以通过以下变量访问此状态:
重要提示:由于下文详细讨论的类型提示问题,这些变量对类型提示器/linter 等工具不可见,它们会认为这些是无效变量。您需要使用 #type: ignore 来满足任何此类系统的要求。
_state_manager._mock_state() 一个 [str, object] 字典,其中存储了所有 actor 状态。通过 _state_manager.save_state(key, value) 或任何其他 statemanager 方法保存的任何变量都作为该键值对存储在字典中。通过 try_get_state 或任何其他 statemanager 方法加载的任何值都从此字典中获取。
_state_manager._mock_timers() 一个 [str, ActorTimerData] 字典,其中保存活动的 actor 定时器。任何会添加或移除定时器的 actor 方法都会从此字典中添加或弹出相应的 ActorTimerData 对象。
_state_manager._mock_reminders() 一个 [str, ActorReminderData] 字典,其中保存活动的 actor 提醒。任何会添加或移除定时器的 actor 方法都会从此字典中添加或弹出相应的 ActorReminderData 对象。
注意:定时器和提醒永远不会实际触发。这些字典的存在仅为了测试应该添加或移除定时器/提醒的方法。如果您需要测试它们应该激活的回调,您应该使用适当的值直接调用它们:
result = await mock_actor.recieve_reminder(name, state, due_time, period, _ttl)
# 直接测试结果,或通过查询 `_state_manager._mock_state` 测试副作用(如更改状态)
用法和限制 为了允许更细粒度的控制,_on_activate 方法不会像 Dapr 初始化新的 Actor 实例时那样自动调用。您应该在测试中根据需要手动调用它。
Mock actor 系统当前的一个限制是它不调用 _on_pre_actor_method 和 _on_post_actor_method 方法。您始终可以手动调用这些方法作为测试的一部分。
__init__、register_timer、unregister_timer、register_reminder、unregister_reminder 方法都会被 MockActor 类覆盖,该类通过 create_mock_actor 作为 mixin 应用。如果您的 actor 本身覆盖了这些方法,这些修改本身将被覆盖,actor 可能不会按您的预期运行。
注意:__init__ 是一个特殊情况,您应该将其定义为
def __init__(self, ctx, actor_id):
super().__init__(ctx, actor_id)
Mock actor 可以正常工作,但如果您在 __init__ 中添加了任何额外逻辑,它将被覆盖。值得注意的是,在初始化时应用逻辑的正确方法是通过 _on_activate(也可以与 mock actor 安全使用),而不是 __init__。
如果您有一个确实覆盖了默认 Dapr actor 方法的 actor,您可以创建 MockActor 类(来自 MockActor.py)的自定义子类,该类实现您拥有的任何自定义逻辑,同时与 _mock_state、_mock_timers 和 _mock_reminders 正常交互,然后通过您自己定义的 create_mock_actor 函数将该自定义类作为 mixin 应用。
Actor _runtime_ctx 变量设置为 None。所有正常的 actor 方法都已覆盖,不会调用它,但如果您的代码本身直接与 _runtime_ctx 交互,测试可能会失败。
Actor _state_manager 被 MockStateManager 实例覆盖。它具有与基本 ActorStateManager 相同的所有方法和功能,除了使用各种 _mock 变量而不是 _runtime_ctx 来存储数据。如果您的代码实现了自己的自定义状态管理器,它将被覆盖,测试可能会失败。
类型提示 由于 Python 缺乏用于类型提示类型交集的统一方法(参见:python/typing #213 ),类型提示不幸地不适用于 Mock Actor。返回类型被类型提示为"Actor 子类 T 的实例",而实际上应该被类型提示为"MockActor 子类 T 的实例"或"类型交集 [Actor 子类 T, MockActor] 的实例"(值得注意的是,MockActor 本身是 Actor 的子类)。
这意味着,例如,如果您在代码编辑器中将鼠标悬停在 mockactor._state_manager 上,它将显示为 ActorStateManager 的实例(而不是 MockStateManager),各种 IDE 辅助功能(如 VSCode 的 Go to Definition,它会将您带到 ActorStateManager 的定义而不是 MockStateManager)将无法正常工作。
目前,这个问题无法修复,因此仅仅需要注意它,因为它可能会引起混淆。如果将来能够准确地为这种情况进行类型提示,欢迎随时打开关于实现它的问题。
6.3 - Dapr Python SDK 扩展 Python SDK,用于开发 Dapr 应用程序
6.3.1 - Dapr Python gRPC 服务扩展入门 如何快速上手 Dapr Python gRPC 扩展
Dapr Python SDK 提供了一个内置的 gRPC 服务器扩展 dapr.ext.grpc,用于创建 Dapr 服务。
安装 您可以通过以下命令下载并安装 Dapr gRPC 服务器扩展:
pip install dapr-ext-grpc
注意 开发包包含的功能和行为将与 Dapr 运行时的预发布版本兼容。在安装 <code>dapr-dev</code> 包之前,请确保卸载 Python SDK 扩展的任何稳定版本。
pip3 install dapr-ext-grpc-dev
示例 App 对象可用于创建服务器。
监听服务调用请求 InvokeMethodReqest 和 InvokeMethodResponse 对象可用于处理传入请求。
一个简单的监听并响应请求的服务如下所示:
from dapr.ext.grpc import App , InvokeMethodRequest , InvokeMethodResponse
app = App ()
@app.method ( name = 'my-method' )
def mymethod ( request : InvokeMethodRequest ) -> InvokeMethodResponse :
print ( request . metadata , flush = True )
print ( request . text (), flush = True )
return InvokeMethodResponse ( b 'INVOKE_RECEIVED' , "text/plain; charset=UTF-8" )
app . run ( 50051 )
完整示例可在此处 找到。
订阅主题 在订阅主题时,您可以指示 Dapr 已接受传递的事件,还是应该丢弃该事件或稍后重试。
from typing import Optional
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
from dapr.clients.grpc._response import TopicEventResponse
app = App ()
# 主题的默认订阅
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' )
def mytopic ( event : v1 . Event ) -> Optional [ TopicEventResponse ]:
print ( event . Data (), flush = True )
# 返回 None(或不显式返回)等效于
# 返回 TopicEventResponse("success")。
# 您也可以返回 TopicEventResponse("retry") 让 dapr 记录
# 该消息并稍后重试传递,或返回 TopicEventResponse("drop")
# 让其丢弃该消息
return TopicEventResponse ( "success" )
# 使用发布订阅路由的特定处理程序
@app.subscribe ( pubsub_name = 'pubsub' , topic = 'TOPIC_A' ,
rule = Rule ( "event.type == \" important \" " , 1 ))
def mytopic_important ( event : v1 . Event ) -> None :
print ( event . Data (), flush = True )
# 禁用主题验证的处理程序
@app.subscribe ( pubsub_name = 'pubsub-mqtt' , topic = 'topic/#' , disable_topic_validation = True ,)
def mytopic_wildcard ( event : v1 . Event ) -> None :
print ( event . Data (), flush = True )
app . run ( 50051 )
完整示例可在此处 找到。
设置输入绑定触发器 from dapr.ext.grpc import App , BindingRequest
app = App ()
@app.binding ( 'kafkaBinding' )
def binding ( request : BindingRequest ):
print ( request . text (), flush = True )
app . run ( 50051 )
完整示例可在此处 找到。
相关链接 6.3.2 - Dapr Python SDK 与 FastAPI 集成 如何使用 FastAPI 扩展创建 Dapr Python virtual actors 和发布订阅
Dapr Python SDK 通过 dapr-ext-fastapi 扩展提供与 FastAPI 的集成。
安装 您可以通过以下命令下载并安装 Dapr FastAPI 扩展:
pip install dapr-ext-fastapi
注意 开发版包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何稳定版本的 Python SDK 扩展。
pip install dapr-ext-fastapi-dev
示例 订阅不同类型的事件 import uvicorn
from fastapi import Body , FastAPI
from dapr.ext.fastapi import DaprApp
from pydantic import BaseModel
class RawEventModel ( BaseModel ):
body : str
class User ( BaseModel ):
id : int
name : str
class CloudEventModel ( BaseModel ):
data : User
datacontenttype : str
id : str
pubsubname : str
source : str
specversion : str
topic : str
traceid : str
traceparent : str
tracestate : str
type : str
app = FastAPI ()
dapr_app = DaprApp ( app )
# 允许处理任何结构的事件(最简单,但最不健壮)
# dapr publish --publish-app-id sample --topic any_topic --pubsub pubsub --data '{"id":"7", "desc": "good", "size":"small"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'any_topic' )
def any_event_handler ( event_data = Body ()):
print ( event_data )
# 为了健壮性,根据发布者是否使用 CloudEvents 选择以下方式之一
# 处理使用 CloudEvents 发送的事件
# dapr publish --publish-app-id sample --topic cloud_topic --pubsub pubsub --data '{"id":"7", "name":"Bob Jones"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'cloud_topic' )
def cloud_event_handler ( event_data : CloudEventModel ):
print ( event_data )
# 处理不使用 CloudEvents 发送的原始事件
# curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/raw_topic?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"body": "345"}'
@dapr_app.subscribe ( pubsub = 'pubsub' , topic = 'raw_topic' )
def raw_event_handler ( event_data : RawEventModel ):
print ( event_data )
if __name__ == "__main__" :
uvicorn . run ( app , host = "0.0.0.0" , port = 30212 )
创建 actor from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from demo_actor import DemoActor
app = FastAPI ( title = f ' { DemoActor . __name__ } Service' )
# 添加 Dapr Actor 扩展
actor = DaprActor ( app )
@app.on_event ( "startup" )
async def startup_event ():
# 注册 DemoActor
await actor . register_actor ( DemoActor )
@app.get ( "/GetMyData" )
def get_my_data ():
return "{'message': 'myData'}"
6.3.3 - Dapr Python SDK 与 Flask 集成 如何使用 Flask 扩展创建 Dapr Python virtual actors
Dapr Python SDK 通过 flask-dapr 扩展提供与 Flask 的集成。
安装 你可以使用以下命令下载并安装 Dapr Flask 扩展:
注意 开发包将包含与 Dapr runtime 预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保卸载任何稳定版本的 Python SDK 扩展。
pip install flask-dapr-dev
示例 from flask import Flask
from flask_dapr.actor import DaprActor
from dapr.conf import settings
from demo_actor import DemoActor
app = Flask ( f ' { DemoActor . __name__ } Service' )
# 启用 DaprActor Flask 扩展
actor = DaprActor ( app )
# 注册 DemoActor
actor . register_actor ( DemoActor )
# 设置方法路由
@app.route ( '/GetMyData' , methods = [ 'GET' ])
def get_my_data ():
return { 'message' : 'myData' }, 200
# 运行应用
if __name__ == '__main__' :
app . run ( port = settings . HTTP_APP_PORT )
6.3.4 - Dapr Python SDK 与 Dapr 工作流扩展集成 如何快速上手使用 Dapr 工作流扩展
Dapr Python SDK 提供了一个内置的 Dapr 工作流扩展 dapr.ext.workflow,用于创建 Dapr 服务。
安装 您可以通过以下命令下载并安装 Dapr 工作流扩展:
pip install dapr-ext-workflow
Note 开发版包将包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 <code>dapr-dev</code> 包之前,请确保已卸载任何稳定版本的 Python SDK 扩展。
pip install dapr-ext-workflow-dev
示例 from time import sleep
import dapr.ext.workflow as wf
wfr = wf . WorkflowRuntime ()
@wfr.workflow ( name = 'random_workflow' )
def task_chain_workflow ( ctx : wf . DaprWorkflowContext , wf_input : int ):
try :
result1 = yield ctx . call_activity ( step1 , input = wf_input )
result2 = yield ctx . call_activity ( step2 , input = result1 )
except Exception as e :
yield ctx . call_activity ( error_handler , input = str ( e ))
raise
return [ result1 , result2 ]
@wfr.activity ( name = 'step1' )
def step1 ( ctx , activity_input ):
print ( f 'Step 1: Received input: { activity_input } .' )
# Do some work
return activity_input + 1
@wfr.activity
def step2 ( ctx , activity_input ):
print ( f 'Step 2: Received input: { activity_input } .' )
# Do some work
return activity_input * 2
@wfr.activity
def error_handler ( ctx , error ):
print ( f 'Executing error handler: { error } .' )
# Do some compensating work
if __name__ == '__main__' :
wfr . start ()
sleep ( 10 ) # wait for workflow runtime to start
wf_client = wf . DaprWorkflowClient ()
instance_id = wf_client . schedule_new_workflow ( workflow = task_chain_workflow , input = 42 )
print ( f 'Workflow started. Instance ID: { instance_id } ' )
state = wf_client . wait_for_workflow_completion ( instance_id )
print ( f 'Workflow completed! Status: { state . runtime_status } ' )
wfr . shutdown ()
后续步骤 Dapr 工作流 Python SDK 入门 6.3.4.1 - Dapr Workflow Python SDK 入门 如何使用 Dapr Python SDK 快速上手工作流
让我们创建一个 Dapr 工作流并通过控制台调用它。借助提供的工作流示例 ,你将:
运行一个 Python 控制台应用程序 ,该程序演示包含活动、子工作流和外部事件的工作流编排 了解如何处理重试、超时以及工作流状态管理 使用 Python 工作流 SDK 来启动、暂停、恢复和清理工作流实例 本示例使用自托管模式 下通过 dapr init 初始化的默认配置。
在 Python 示例项目中,simple.py 文件包含应用程序的设置,包括:
前置条件 设置环境 首先克隆 [Python SDK 仓库]。
git clone https://github.com/dapr/python-sdk.git
从 Python SDK 根目录导航到 Dapr Workflow 示例。
运行以下命令,安装使用 Dapr Python SDK 运行此工作流示例所需的所有依赖。
pip3 install -r workflow/requirements.txt
在本地运行应用程序 要运行 Dapr 应用程序,你需要启动 Python 程序和一个 Dapr 边车。在终端中运行:
dapr run --app-id wf-simple-example --dapr-grpc-port 50001 --resources-path components -- python3 simple.py
注意: 由于 Windows 上未定义 Python3.exe,你可能需要使用 python simple.py 而不是 python3 simple.py。
预期输出
- "== APP == Hi Counter!"
- "== APP == New counter value is: 1!"
- "== APP == New counter value is: 11!"
- "== APP == Retry count value is: 0!"
- "== APP == Retry count value is: 1! This print statement verifies retry"
- "== APP == Appending 1 to child_orchestrator_string!"
- "== APP == Appending a to child_orchestrator_string!"
- "== APP == Appending a to child_orchestrator_string!"
- "== APP == Appending 2 to child_orchestrator_string!"
- "== APP == Appending b to child_orchestrator_string!"
- "== APP == Appending b to child_orchestrator_string!"
- "== APP == Appending 3 to child_orchestrator_string!"
- "== APP == Appending c to child_orchestrator_string!"
- "== APP == Appending c to child_orchestrator_string!"
- "== APP == Get response from hello_world_wf after pause call: Suspended"
- "== APP == Get response from hello_world_wf after resume call: Running"
- "== APP == New counter value is: 111!"
- "== APP == New counter value is: 1111!"
- "== APP == Workflow completed! Result: "Completed"
发生了什么? 当你运行应用程序时,会演示几个关键的工作流功能:
工作流和活动注册 :应用程序使用 Python 装饰器自动向运行时注册工作流和活动。这种基于装饰器的方法提供了一种简洁、声明式的方式来定义你的工作流组件:
@wfr.workflow ( name = 'hello_world_wf' )
def hello_world_wf ( ctx : DaprWorkflowContext , wf_input ):
# Workflow definition...
@wfr.activity ( name = 'hello_act' )
def hello_act ( ctx : WorkflowActivityContext , wf_input ):
# Activity definition...
运行时设置 :应用程序初始化工作流运行时和客户端:
wfr = WorkflowRuntime ()
wfr . start ()
wf_client = DaprWorkflowClient ()
活动执行 :工作流执行一系列活动来递增计数器:
@wfr.workflow ( name = 'hello_world_wf' )
def hello_world_wf ( ctx : DaprWorkflowContext , wf_input ):
yield ctx . call_activity ( hello_act , input = 1 )
yield ctx . call_activity ( hello_act , input = 10 )
重试逻辑 :工作流演示了使用重试策略进行错误处理:
retry_policy = RetryPolicy (
first_retry_interval = timedelta ( seconds = 1 ),
max_number_of_attempts = 3 ,
backoff_coefficient = 2 ,
max_retry_interval = timedelta ( seconds = 10 ),
retry_timeout = timedelta ( seconds = 100 ),
)
yield ctx . call_activity ( hello_retryable_act , retry_policy = retry_policy )
子工作流 :子工作流使用自己的重试策略执行:
yield ctx . call_child_workflow ( child_retryable_wf , retry_policy = retry_policy )
外部事件处理 :工作流等待一个带有超时的外部事件:
event = ctx . wait_for_external_event ( event_name )
timeout = ctx . create_timer ( timedelta ( seconds = 30 ))
winner = yield when_any ([ event , timeout ])
工作流生命周期管理 :示例演示如何暂停和恢复工作流:
wf_client . pause_workflow ( instance_id = instance_id )
metadata = wf_client . get_workflow_state ( instance_id = instance_id )
# ... check status ...
wf_client . resume_workflow ( instance_id = instance_id )
事件触发 :恢复后,工作流触发一个事件:
wf_client . raise_workflow_event (
instance_id = instance_id ,
event_name = event_name ,
data = event_data
)
完成与清理 :最后,工作流等待完成并进行清理:
state = wf_client . wait_for_workflow_completion (
instance_id ,
timeout_in_seconds = 30
)
wf_client . purge_workflow ( instance_id = instance_id )
后续步骤 6.4 - title: “Conversation API(Python)- 推荐用法”
linkTitle: “对话”
weight: 11000
type: docs
description: 推荐在 Python 中配合或不配合工具使用 Dapr Conversation API 的模式,包括多轮流程与安全指引。 Dapr Conversation API 当前仍处于 alpha 阶段。本文给出在 Python SDK 中高效使用它的推荐最小模式:
纯请求(无工具) 携带工具的请求(将函数用作工具) 带工具执行的多轮流程 异步变体 执行工具调用时的重要安全注意事项 前提条件 如需完整的端到端流程与提供程序配置,参见:
基础对话(无工具) from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
# 构建单轮 Alpha2 输入
user_msg = conversation . create_user_message ( "什么是 Dapr?" )
alpha2_input = conversation . ConversationInputAlpha2 ( messages = [ user_msg ])
with DaprClient () as client :
resp = client . converse_alpha2 (
name = "echo" , # 替换为你的 LLM 组件名称
inputs = [ alpha2_input ],
temperature = 1 ,
)
for msg in resp . to_assistant_messages ():
if msg . of_assistant . content :
print ( msg . of_assistant . content [ 0 ] . text )
要点:
使用 conversation.create_user_message 构造消息。 将其包进 ConversationInputAlpha2(messages=[...]),再传给 converse_alpha2。 使用 response.to_assistant_messages() 遍历 assistant 输出。 工具:基于装饰器(推荐) 基于装饰器的工具方式更整洁、也更顺手。定义函数时应写清晰的类型提示与详细 docstring;这对 LLM 理解何时以及如何调用该工具很重要;
然后使用 @conversation.tool 进行装饰。注册后的工具可以传给 LLM,并通过工具调用来执行。
from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_weather ( location : str , unit : str = 'fahrenheit' ) -> str :
"""获取指定地点的当前天气。"""
# 请替换为真实实现
return f "Weather in { location } (unit= { unit } )"
user_msg = conversation . create_user_message ( "巴黎的天气怎么样?" )
alpha2_input = conversation . ConversationInputAlpha2 ( messages = [ user_msg ])
with DaprClient () as client :
response = client . converse_alpha2 (
name = "openai" , # 你的 LLM 组件
inputs = [ alpha2_input ],
tools = conversation . get_registered_tools (), # 由 @conversation.tool 注册的工具
tool_choice = 'auto' ,
temperature = 1 ,
)
# 检查 assistant 消息,包括其中的工具调用
for msg in response . to_assistant_messages ():
if msg . of_assistant . tool_calls :
for tc in msg . of_assistant . tool_calls :
print ( f "Tool call: { tc . function . name } args= { tc . function . arguments } " )
elif msg . of_assistant . content :
print ( msg . of_assistant . content [ 0 ] . text )
说明:
使用 conversation.get_registered_tools() 收集所有通过 @conversation.tool 装饰的函数。 绑定器会依据函数签名校验并强制转换参数。类型注解要准确。 使用工具的最小多轮流程 这是处理带工具对话时最常用的循环:
警告 不要在不信任所有已注册工具的情况下,盲目自动执行 LLM 返回的工具调用。工具名和参数都应视为不可信输入。
请校验输入并施加防护(工具白名单、参数 schema、副作用约束)。 对异步或 I/O 密集型工具,优先使用 conversation.execute_registered_tool_async(..., timeout=...),并设置保守的超时时间。 在敏感场景中,考虑在执行前增加策略层或用户确认步骤。 记录并监控工具使用;当校验失败时应默认拒绝执行。 from dapr.clients import DaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_weather ( location : str , unit : str = 'fahrenheit' ) -> str :
return f "Weather in { location } (unit= { unit } )"
history : list [ conversation . ConversationMessage ] = [
conversation . create_user_message ( "旧金山的天气怎么样?" )]
with DaprClient () as client :
# 第 1 轮
resp1 = client . converse_alpha2 (
name = "openai" ,
inputs = [ conversation . ConversationInputAlpha2 ( messages = history )],
tools = conversation . get_registered_tools (),
tool_choice = 'auto' ,
temperature = 1 ,
)
# 追加 assistant 消息;执行工具调用;再追加工具结果
for msg in resp1 . to_assistant_messages ():
history . append ( msg )
for tc in msg . of_assistant . tool_calls :
# 重要:生产环境中必须校验输入并施加防护
tool_output = conversation . execute_registered_tool (
tc . function . name , tc . function . arguments
)
history . append (
conversation . create_tool_message (
tool_id = tc . id , name = tc . function . name , content = str ( tool_output )
)
)
# 第 2 轮(LLM 会看到工具结果)
history . append ( conversation . create_user_message ( "我需要带伞吗?" ))
resp2 = client . converse_alpha2 (
name = "openai" ,
inputs = [ conversation . ConversationInputAlpha2 ( messages = history )],
tools = conversation . get_registered_tools (),
temperature = 1 ,
)
for msg in resp2 . to_assistant_messages ():
history . append ( msg )
if not msg . of_assistant . tool_calls and msg . of_assistant . content :
print ( msg . of_assistant . content [ 0 ] . text )
提示:
始终把 assistant 消息追加到 history。 执行每个工具调用时,都要先校验,再把工具输出作为 tool message 追加进去。 下一轮请求会带上这些工具结果,LLM 才能基于它们继续推理。 将函数用作工具:其他方式 当装饰器方式不方便时,还有两种选择。
A)从带类型的函数自动生成 schema:
from enum import Enum
from dapr.clients.grpc import conversation
class Units ( Enum ):
CELSIUS = 'celsius'
FAHRENHEIT = 'fahrenheit'
def get_weather ( location : str , unit : Units = Units . FAHRENHEIT ) -> str :
return f "Weather in { location } "
fn = conversation . ConversationToolsFunction . from_function ( get_weather )
weather_tool = conversation . ConversationTools ( function = fn )
B)手写 JSON Schema(兜底方式):
from dapr.clients.grpc import conversation
fn = conversation . ConversationToolsFunction (
name = 'get_weather' ,
description = 'Get current weather' ,
parameters = {
'type' : 'object' ,
'properties' : {
'location' : { 'type' : 'string' },
'unit' : { 'type' : 'string' , 'enum' : [ 'celsius' , 'fahrenheit' ]},
},
'required' : [ 'location' ],
},
)
weather_tool = conversation . ConversationTools ( function = fn )
异步变体 按需使用异步客户端与异步工具执行辅助方法。
import asyncio
from dapr.aio.clients import DaprClient as AsyncDaprClient
from dapr.clients.grpc import conversation
@conversation.tool
def get_time () -> str :
return '2025-01-01T12:00:00Z'
async def main ():
async with AsyncDaprClient () as client :
msg = conversation . create_user_message ( '现在几点?' )
inp = conversation . ConversationInputAlpha2 ( messages = [ msg ])
resp = await client . converse_alpha2 (
name = 'openai' , inputs = [ inp ], tools = conversation . get_registered_tools ()
)
for m in resp . to_assistant_messages ():
if m . of_assistant . content :
print ( m . of_assistant . content [ 0 ] . text )
asyncio . run ( main ())
如果你需要异步执行工具(例如网络 I/O),请实现异步函数,并结合超时参数使用 conversation.execute_registered_tool_async。
安全与验证(必读) LLM 可能会建议调用工具。所有由模型提供的参数都必须视为不可信输入。
建议:
仅将可信函数注册为工具。为清晰性与自动 schema 生成,优先使用 @conversation.tool 装饰器。 使用精确的类型注解和 docstring。SDK 会把函数签名转换成 JSON schema,并在参数绑定时执行类型强制转换,同时拒绝意外或无效字段。 对可能产生副作用的工具(文件系统、网络、子进程)添加防护。可考虑白名单、沙箱和配额限制。 执行前校验参数。例如清理文件路径,或限制 URL / 域名范围。 考虑超时和并发控制。对于异步工具,可向 execute_registered_tool_async(..., timeout=...) 传入超时。 记录并监控工具使用。默认拒绝:若校验失败,就不要执行工具,并以安全方式告知用户。 另请参阅 dapr/clients/grpc/conversation.py 中的内联说明(如 tool()、ConversationTools、execute_registered_tool),了解参数绑定与错误处理细节。
关键辅助方法(速查) 本节汇总了示例中使用到的 dapr.clients.grpc.conversation 辅助工具。
create_user_message(text: str) -> ConversationMessage
为 Alpha2 构建 user 角色消息。可用于 history 列表。 示例:history.append(conversation.create_user_message("Hello")) create_system_message(text: str) -> ConversationMessage
构建 system 消息,用于约束 assistant 的行为。 示例:history = [conversation.create_system_message("You are a concise assistant.")] create_assistant_message(text: str) -> ConversationMessage
适合在测试或受控流程中注入 assistant 文本。 create_tool_message(tool_id: str, name: str, content: Any) -> ConversationMessage
将工具输出转换为下一轮可供 LLM 读取的 tool message。 content 可以是任意对象;SDK 会安全地将其转为字符串。 示例:history.append(conversation.create_tool_message(tool_id=tc.id, name=tc.function.name, content=conversation.execute_registered_tool(tc.function.name, tc.function.arguments))) get_registered_tools() -> list[ConversationTools]
返回当前进程内注册表中的全部工具。 包括以下方式创建的工具:@conversation.tool 装饰器(默认自动注册),以及ConversationToolsFunction.from_function 且 register=True(默认)。 在 converse_alpha2(..., tools=...) 中传入该列表。 register_tool(name: str, t: ConversationTools) / unregister_tool(name: str)
手动管理工具注册表(例如高级场景、测试、清理)。 名称必须唯一;在长生命周期进程中,记得注销以避免冲突。 execute_registered_tool(name: str, params: Mapping|Sequence|str|None) -> Any
按名称同步执行已注册工具。 params 可接受 kwargs(mapping)、args(sequence)、JSON 字符串或 None。若传入 JSON 字符串(LLM 常见返回形式),SDK 会自动解析。 参数会依据函数签名或 schema 进行校验与强制转换;多余或无效字段会直接报错。 安全性:params 必须视为不可信输入;对副作用操作要加防护。 execute_registered_tool_async(name: str, params: Mapping|Sequence|str|None, *, timeout: float|None=None) -> Any
异步版本。支持超时;对 I/O 密集型工具尤其建议使用。 适用于异步工具,或配合 aio 客户端使用。 ConversationToolsFunction.from_function(func: Callable, register: bool = True) -> ConversationToolsFunction
从带类型的 Python 函数(注解 + 可选 docstring)推导 JSON schema,并可选择直接注册为工具。 常见用法:spec = conversation.ConversationToolsFunction.from_function(my_func);随后可依赖自动注册,也可用 ConversationTools(function=spec) 包装后调用 register_tool(spec.name, tool),或直接把 [tool] 传给 tools=。 ConversationResponseAlpha2.to_assistant_messages() -> list[ConversationMessage]
便捷方法,用于把响应输出转换为 assistant 的 ConversationMessage 对象,便于直接追加到 history(包括存在的 tool_calls)。 提示:@conversation.tool 装饰器是创建工具最简单的方式。它会根据函数自动生成 schema,支持可选的命名空间或名称覆盖,并自动注册工具(若想延后注册,可设置 register=False)。
7 - Dapr Rust SDK 用于开发 Dapr 应用程序的 Rust SDK 软件包
Note Dapr Rust SDK 目前处于 Alpha 阶段。目前正在努力将其推向稳定版本,可能会涉及破坏性变更。一个帮助使用 Rust 构建 Dapr 应用程序的客户端库。该客户端旨在支持所有公共 Dapr API,同时专注于惯用的 Rust 体验和开发者生产力。
7.1 - 开始使用 Dapr 客户端 Rust SDK 如何开始使用 Dapr Rust SDK
Dapr 客户端包允许您从 Rust 应用程序与其他 Dapr 应用程序进行交互。
注意 Dapr Rust SDK 目前处于 Alpha 阶段。我们正在努力将其推向稳定版本,期间可能会涉及破坏性变更。前置条件 导入客户端包 将 Dapr 添加到您的 cargo.toml
[ dependencies ]
dapr = "0.16"
您可以直接引用 dapr::Client 或将完整路径绑定到新名称,如下所示:
use dapr ::Client as DaprClient ;
实例化 Dapr 客户端 let addr = "https://127.0.0.1" . to_string ();
let mut client = dapr ::Client ::< dapr ::client ::TonicClient > ::connect ( addr ,
port ). await ? ;
或者,如果您想指定自定义端口,可以使用此 connect 方法:
let mut client = dapr ::Client ::< dapr ::client ::TonicClient > ::connect_with_port ( addr , "3500" . to_string ()). await ? ;
构建块 Rust SDK 允许您与 Dapr 构建块 进行交互。
服务调用 (gRPC) 要在运行 Dapr 边车的另一个服务上调用特定方法,Dapr 客户端提供两个选项:
调用 (gRPC) 服务
let response = client
. invoke_service ( "service-to-invoke" , "method-to-invoke" , Some ( data ))
. await
. unwrap ();
有关服务调用的完整指南,请访问如何操作:调用服务 。
状态管理 Dapr 客户端提供对这些状态管理方法的访问:save_state、get_state、delete_state,可以像这样使用:
let store_name = String ::from ( "statestore" );
let key = String ::from ( "hello" );
let val = String ::from ( "world" ). into_bytes ();
// save key-value pair in the state store
client
. save_state ( store_name , key , val , None , None , None )
. await ? ;
let get_response = client
. get_state ( "statestore" , "hello" , None )
. await ? ;
// delete a value from the state store
client
. delete_state ( "statestore" , "hello" , None )
. await ? ;
可以使用 save_bulk_states 方法发送多个状态。
有关状态管理的完整指南,请访问如何操作:保存和获取状态 。
发布消息 要将数据发布到主题,Dapr 客户端提供了一个简单的方法:
let pubsub_name = "pubsub-name" . to_string ();
let pubsub_topic = "topic-name" . to_string ();
let pubsub_content_type = "text/plain" . to_string ();
let data = "content" . to_string (). into_bytes ();
client
. publish_event ( pubsub_name , pubsub_topic , pubsub_content_type , data , None )
. await ? ;
有关发布订阅的完整指南,请访问如何操作:发布和订阅 。
相关链接 Rust SDK 示例