Dapr .NET SDK 用于开发 Dapr 应用程序的 .NET SDK 包
Dapr 提供多种包以协助 .NET 应用程序的开发。通过这些包,您可以使用 Dapr 创建 .NET 客户端、服务器和虚拟 actor。
先决条件
注意 请注意,虽然 .NET 6 通常是 Dapr .NET SDK 包的最低 .NET 要求,并且 .NET 7 是 Dapr v1.15 中 Dapr.Workflows 的最低支持版本,但从 v1.16 开始,Dapr 仅支持 .NET 8 和 .NET 9。安装 要开始使用 Client .NET SDK,请安装 Dapr .NET SDK 包:
dotnet add package Dapr.Client
体验 尝试 Dapr .NET SDK。通过 .NET 快速入门和教程来探索 Dapr 的实际应用:
SDK 示例 描述 快速入门 使用 .NET SDK 在几分钟内体验 Dapr 的 API 构建块。 SDK 示例 克隆 SDK 仓库以尝试一些示例并开始使用。 发布/订阅教程 查看 Dapr .NET SDK 如何与其他 Dapr SDK 一起工作以启用发布/订阅应用程序。
可用包 客户端 创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 .NET 客户端。
服务器 使用 Dapr SDK 编写 .NET 服务器和服务。包括对 ASP.NET 的支持。
Actors 在 .NET 中创建具有状态、提醒/计时器和方法的虚拟 actor。
工作流 创建和管理与其他 Dapr API 一起工作的工作流。
更多信息 了解更多关于本地开发选项的信息,或浏览 NuGet 包以添加到您现有的 .NET 应用程序中。
开发 了解 .NET Dapr 应用程序的本地开发选项
NuGet 包 用于将 .NET SDK 添加到您的 .NET 应用程序的 Dapr 包。
1 - 开始使用 Dapr 客户端 .NET SDK 如何使用 Dapr .NET SDK 快速上手
Dapr 客户端包使您能够从 .NET 应用程序与其他 Dapr 应用程序进行交互。
注意 如果您还没有这样做,
请尝试其中一个快速入门 ,以快速了解如何使用 Dapr .NET SDK 和 API 构建块。
构建块 .NET SDK 允许您与所有 Dapr 构建块 进行接口交互。
调用服务 HTTP 您可以使用 DaprClient
或 System.Net.Http.HttpClient
来调用服务。
using var client = new DaprClientBuilder ().
UseTimeout ( TimeSpan . FromSeconds ( 2 )). // 可选:设置超时
Build ();
// 调用名为 "deposit" 的 POST 方法,输入类型为 "Transaction"
var data = new { id = "17" , amount = 99 m };
var account = await client . InvokeMethodAsync < object , Account >( "routing" , "deposit" , data , cancellationToken );
Console . WriteLine ( "返回: id:{0} | 余额:{1}" , account . Id , account . Balance );
var client = DaprClient . CreateInvokeHttpClient ( appId : "routing" );
// 设置 HTTP 客户端的超时:
client . Timeout = TimeSpan . FromSeconds ( 2 );
var deposit = new Transaction { Id = "17" , Amount = 99 m };
var response = await client . PostAsJsonAsync ( "/deposit" , deposit , cancellationToken );
var account = await response . Content . ReadFromJsonAsync < Account >( cancellationToken : cancellationToken );
Console . WriteLine ( "返回: id:{0} | 余额:{1}" , account . Id , account . Balance );
gRPC 您可以使用 DaprClient
通过 gRPC 调用服务。
using var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 20 ));
var invoker = DaprClient . CreateInvocationInvoker ( appId : myAppId , daprEndpoint : serviceEndpoint );
var client = new MyService . MyServiceClient ( invoker );
var options = new CallOptions ( cancellationToken : cts . Token , deadline : DateTime . UtcNow . AddSeconds ( 1 ));
await client . MyMethodAsync ( new Empty (), options );
Assert . Equal ( StatusCode . DeadlineExceeded , ex . StatusCode );
保存和获取应用程序状态 var client = new DaprClientBuilder (). Build ();
var state = new Widget () { Size = "small" , Color = "yellow" , };
await client . SaveStateAsync ( storeName , stateKeyName , state , cancellationToken : cancellationToken );
Console . WriteLine ( "状态已保存!" );
state = await client . GetStateAsync < Widget >( storeName , stateKeyName , cancellationToken : cancellationToken );
Console . WriteLine ( $"获取状态: {state.Size} {state.Color}" );
await client . DeleteStateAsync ( storeName , stateKeyName , cancellationToken : cancellationToken );
Console . WriteLine ( "状态已删除!" );
查询状态 (Alpha) var query = "{" +
"\"filter\": {" +
"\"EQ\": { \"value.Id\": \"1\" }" +
"}," +
"\"sort\": [" +
"{" +
"\"key\": \"value.Balance\"," +
"\"order\": \"DESC\"" +
"}" +
"]" +
"}" ;
var client = new DaprClientBuilder (). Build ();
var queryResponse = await client . QueryStateAsync < Account >( "querystore" , query , cancellationToken : cancellationToken );
Console . WriteLine ( $"获取 {queryResponse.Results.Count}" );
foreach ( var account in queryResponse . Results )
{
Console . WriteLine ( $"账户: {account.Data.Id} 余额 {account.Data.Balance}" );
}
发布消息 var client = new DaprClientBuilder (). Build ();
var eventData = new { Id = "17" , Amount = 10 m , };
await client . PublishEventAsync ( pubsubName , "deposit" , eventData , cancellationToken );
Console . WriteLine ( "已发布存款事件!" );
与输出绑定交互 using var client = new DaprClientBuilder (). Build ();
// Twilio SendGrid 绑定的示例负载
var email = new
{
metadata = new
{
emailTo = "customer@example.com" ,
subject = "来自 Dapr SendGrid 绑定的邮件" ,
},
data = "<h1>测试 Dapr 绑定</h1>这是一个测试。<br>再见!" ,
};
await client . InvokeBindingAsync ( "send-email" , "create" , email );
检索秘密
Multi-value-secret
Single-value-secret var client = new DaprClientBuilder (). Build ();
// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client . GetSecretAsync ( "mysecretstore" , "key-value-pair-secret" );
Console . WriteLine ( $"获取秘密键: {string.Join(" , ", secrets.Keys)}" );
var client = new DaprClientBuilder (). Build ();
// 检索基于键值对的秘密 - 返回一个 Dictionary<string, string>
var secrets = await client . GetSecretAsync ( "mysecretstore" , "key-value-pair-secret" );
Console . WriteLine ( $"获取秘密键: {string.Join(" , ", secrets.Keys)}" );
// 检索单值秘密 - 返回一个 Dictionary<string, string>
// 包含一个以秘密名称为键的单个值
var data = await client . GetSecretAsync ( "mysecretstore" , "single-value-secret" );
var value = data [ "single-value-secret" ]
Console . WriteLine ( "获取了一个秘密值,我不会打印它,因为它是秘密!" );
获取配置键 var client = new DaprClientBuilder (). Build ();
// 检索特定的一组键。
var specificItems = await client . GetConfiguration ( "configstore" , new List < string >() { "key1" , "key2" });
Console . WriteLine ( $"这是我的值:\n{specificItems[0].Key} -> {specificItems[0].Value}\n{specificItems[1].Key} -> {specificItems[1].Value}" );
// 通过提供一个空列表来检索所有配置项。
var specificItems = await client . GetConfiguration ( "configstore" , new List < string >());
Console . WriteLine ( $"我得到了 {configItems.Count} 个条目!" );
foreach ( var item in configItems )
{
Console . WriteLine ( $"{item.Key} -> {item.Value}" )
}
订阅配置键 var client = new DaprClientBuilder (). Build ();
// 订阅配置 API 返回一个 IAsyncEnumerable<IEnumerable<ConfigurationItem>> 的包装器。
// 通过在 foreach 循环中访问其 Source 进行迭代。当流被切断或取消令牌被取消时,循环将结束。
var subscribeConfigurationResponse = await daprClient . SubscribeConfiguration ( store , keys , metadata , cts . Token );
await foreach ( var items in subscribeConfigurationResponse . Source . WithCancellation ( cts . Token ))
{
foreach ( var item in items )
{
Console . WriteLine ( $"{item.Key} -> {item.Value}" )
}
}
分布式锁 (Alpha) 获取锁 using System ;
using Dapr.Client ;
namespace LockService
{
class Program
{
[Obsolete("分布式锁 API 处于 Alpha 阶段,一旦稳定可以移除。")]
static async Task Main ( string [] args )
{
var daprLockName = "lockstore" ;
var fileName = "my_file_name" ;
var client = new DaprClientBuilder (). Build ();
// 使用这种方法锁定也会自动解锁,因为这是一个可释放对象
await using ( var fileLock = await client . Lock ( DAPR_LOCK_NAME , fileName , "random_id_abc123" , 60 ))
{
if ( fileLock . Success )
{
Console . WriteLine ( "成功" );
}
else
{
Console . WriteLine ( $"锁定 {fileName} 失败。" );
}
}
}
}
}
解锁现有锁 using System ;
using Dapr.Client ;
namespace LockService
{
class Program
{
static async Task Main ( string [] args )
{
var daprLockName = "lockstore" ;
var client = new DaprClientBuilder (). Build ();
var response = await client . Unlock ( DAPR_LOCK_NAME , "my_file_name" , "random_id_abc123" ));
Console . WriteLine ( response . status );
}
}
}
管理工作流实例 (Alpha) var daprClient = new DaprClientBuilder (). Build ();
string instanceId = "MyWorkflowInstance1" ;
string workflowComponentName = "dapr" ; // 或者,这可以是 yaml 中定义的工作流组件的名称
string workflowName = "MyWorkflowDefinition" ;
var input = new { name = "Billy" , age = 30 }; // 任何 JSON 可序列化的值都可以
// 启动工作流
var startResponse = await daprClient . StartWorkflowAsync ( instanceId , workflowComponentName , workflowName , input );
// 终止工作流
await daprClient . TerminateWorkflowAsync ( instanceId , workflowComponentName );
// 获取工作流元数据
var getResponse = await daprClient . GetWorkflowAsync ( instanceId , workflowComponentName , workflowName );
Sidecar APIs Sidecar 健康 .NET SDK 提供了一种轮询 sidecar 健康状态的方法,以及一个等待 sidecar 准备就绪的便捷方法。
轮询健康状态 当 sidecar 和您的应用程序都启动(完全初始化)时,此健康端点返回 true。
var client = new DaprClientBuilder (). Build ();
var isDaprReady = await client . CheckHealthAsync ();
if ( isDaprReady )
{
// 执行依赖 Dapr 的代码。
}
轮询健康状态(出站) 当 Dapr 初始化了其所有组件时,此健康端点返回 true,但可能尚未完成与您的应用程序的通信通道设置。
当您希望在启动路径中利用 Dapr 组件时,这种方法最好,例如,从 secretstore 加载秘密。
var client = new DaprClientBuilder (). Build ();
var isDaprComponentsReady = await client . CheckOutboundHealthAsync ();
if ( isDaprComponentsReady )
{
// 执行依赖 Dapr 组件的代码。
}
等待 sidecar DaprClient
还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,建议包含一个 CancellationToken
以允许请求超时。以下是 DaprSecretStoreConfigurationProvider
中使用此方法的示例。
// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
using ( var tokenSource = new CancellationTokenSource ( sidecarWaitTimeout ))
{
await client . WaitForSidecarAsync ( tokenSource . Token );
}
// 在此处执行 Dapr 组件操作,例如获取秘密。
关闭 sidecar var client = new DaprClientBuilder (). Build ();
await client . ShutdownSidecarAsync ();
相关链接 1.1 - DaprClient 使用 使用 DaprClient 的基本提示和建议
生命周期管理 DaprClient
使用 TCP 套接字来访问网络资源,与 Dapr sidecar 进行通信。它实现了 IDisposable
接口,以便快速清理资源。
依赖注入 通过 AddDaprClient()
方法可以在 ASP.NET Core 中注册 Dapr 客户端。此方法接受一个可选的配置委托,用于配置 DaprClient
,以及一个 ServiceLifetime
参数,允许您为注册的资源指定不同的生命周期,默认是 Singleton
。
以下示例展示了如何使用默认值注册 DaprClient
:
services . AddDaprClient ();
您可以通过配置委托在 DaprClientBuilder
上指定选项来配置 DaprClient
,例如:
services . AddDaprClient ( daprBuilder => {
daprBuilder . UseJsonSerializerOptions ( new JsonSerializerOptions {
WriteIndented = true ,
MaxDepth = 8
});
daprBuilder . UseTimeout ( TimeSpan . FromSeconds ( 30 ));
});
另一个重载允许访问 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
对象的设置是独立的,创建后无法更改。
var daprClient = new DaprClientBuilder ()
. UseJsonSerializerSettings ( ... ) // 配置 JSON 序列化器
. Build ();
默认情况下,DaprClientBuilder
会按以下顺序优先获取配置值:
直接提供给 DaprClientBuilder
方法的值(例如 UseTimeout(TimeSpan.FromSeconds(30))
) 从可选的 IConfiguration
中提取的值,与环境变量名称匹配 从环境变量中提取的值 默认值 在 DaprClientBuilder
上配置 DaprClientBuilder
提供以下方法来设置配置选项:
UseHttpEndpoint(string)
: 设置 Dapr sidecar 的 HTTP 端点UseGrpcEndpoint(string)
: 设置 Dapr sidecar 的 gRPC 端点UseGrpcChannelOptions(GrpcChannelOptions)
: 设置 gRPC 通道选项UseHttpClientFactory(IHttpClientFactory)
: 配置 DaprClient
使用的 HttpClient
工厂UseJsonSerializationOptions(JsonSerializerOptions)
: 配置 JSON 序列化UseDaprApiToken(string)
: 为 Dapr sidecar 的身份验证提供令牌UseTimeout(TimeSpan)
: 指定与 Dapr sidecar 通信时的超时值从 IConfiguration
配置 除了直接从环境变量获取配置值,您还可以通过 IConfiguration
提供这些值。
例如,在多租户环境中,您可能需要为环境变量添加前缀。以下示例展示了如何从环境变量中获取这些值到 IConfiguration
,并移除前缀:
var builder = WebApplication . CreateBuilder ( args );
builder . Configuration . AddEnvironmentVariables ( "test_" ); // 获取所有以 "test_" 开头的环境变量,并移除前缀
builder . Services . AddDaprClient ();
从环境变量配置 SDK 会读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
: Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
: Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
: 如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找本地 HTTP 端点DAPR_GRPC_PORT
: 如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找本地 gRPC 端点DAPR_API_TOKEN
: 设置 API 令牌
注意 如果同时指定了 DAPR_HTTP_ENDPOINT
和 DAPR_HTTP_PORT
,则会忽略 DAPR_HTTP_PORT
的端口值,而使用 DAPR_HTTP_ENDPOINT
上定义的端口。DAPR_GRPC_ENDPOINT
和 DAPR_GRPC_PORT
也是如此。配置 gRPC 通道选项 Dapr 使用 CancellationToken
进行取消,依赖于 gRPC 通道选项的配置,默认已启用。如果您需要自行配置这些选项,请确保启用 ThrowOperationCanceledOnCancellation 设置 。
var daprClient = new DaprClientBuilder ()
. UseGrpcChannelOptions ( new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
. Build ();
使用 DaprClient 进行取消 在 DaprClient 上执行异步操作的 API 接受一个可选的 CancellationToken
参数。这遵循 .NET 的标准惯例,用于可取消的操作。请注意,当取消发生时,不能保证远程端点停止处理请求,只能保证客户端已停止等待完成。
当操作被取消时,将抛出一个 OperationCancelledException
。
理解 DaprClient 的 JSON 序列化 DaprClient
上的许多方法使用 System.Text.Json
序列化器执行 JSON 序列化。接受应用程序数据类型作为参数的方法将对其进行 JSON 序列化,除非文档明确说明了其他情况。
如果您有高级需求,建议阅读 System.Text.Json 文档 。Dapr .NET SDK 不提供独特的序列化行为或自定义 - 它依赖于底层序列化器将数据转换为和从应用程序的 .NET 类型。
DaprClient
被配置为使用从 JsonSerializerDefaults.Web 配置的序列化器选项对象。这意味着 DaprClient
将使用 camelCase
作为属性名称,允许读取带引号的数字("10.99"
),并将不区分大小写地绑定属性。这些是与 ASP.NET Core 和 System.Text.Json.Http
API 一起使用的相同设置,旨在遵循可互操作的 Web 约定。
截至 .NET 5.0,System.Text.Json
对所有 F# 语言特性内置支持不佳。如果您使用 F#,您可能需要使用一个添加对 F# 特性支持的转换器包,例如 FSharp.SystemTextJson 。
JSON 序列化的简单指导 如果您使用的功能集映射到 JSON 的类型系统,您在使用 JSON 序列化和 DaprClient
时的体验将会很顺利。这些是可以简化代码的通用指南。
避免继承和多态 不要尝试序列化具有循环引用的数据 不要在构造函数或属性访问器中放置复杂或昂贵的逻辑 使用与 JSON 类型(数值类型、字符串、DateTime
)清晰映射的 .NET 类型 为顶级消息、事件或状态值创建自己的类,以便将来可以添加属性 设计具有 get
/set
属性的类型,或者使用 支持的模式 用于 JSON 的不可变类型 多态性和序列化 DaprClient
使用的 System.Text.Json
序列化器在执行序列化时使用值的声明类型。
本节将使用 DaprClient.SaveStateAsync<TValue>(...)
作为示例,但建议适用于 SDK 暴露的任何 Dapr 构建块。
public class Widget
{
public string Color { get ; set ; }
}
...
// 将 Widget 值作为 JSON 存储在状态存储中
Widget widget = new Widget () { Color = "Green" , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
在上面的示例中,类型参数 TValue
的类型参数是从 widget
变量的类型推断出来的。这很重要,因为 System.Text.Json
序列化器将根据值的声明类型 执行序列化。结果是 JSON 值 { "color": "Green" }
将被存储。
考虑当您尝试使用 Widget
的派生类型时会发生什么:
public class Widget
{
public string Color { get ; set ; }
}
public class SuperWidget : Widget
{
public bool HasSelfCleaningFeature { get ; set ; }
}
...
// 将 SuperWidget 值作为 JSON 存储在状态存储中
Widget widget = new SuperWidget () { Color = "Green" , HasSelfCleaningFeature = true , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
在此示例中,我们使用了一个 SuperWidget
,但变量的声明类型是 Widget
。由于 JSON 序列化器的行为由声明类型决定,它只看到一个简单的 Widget
,并将保存值 { "color": "Green" }
,而不是 { "color": "Green", "hasSelfCleaningFeature": true }
。
如果您希望 SuperWidget
的属性被序列化,那么最好的选择是用 object
覆盖类型参数。这将导致序列化器包含所有数据,因为它对类型一无所知。
Widget widget = new SuperWidget () { Color = "Green" , HasSelfCleaningFeature = true , };
await client . SaveStateAsync < object >( "mystatestore" , "mykey" , widget );
错误处理 当遇到故障时,DaprClient
上的方法将抛出 DaprException
或其子类。
try
{
var widget = new Widget () { Color = "Green" , };
await client . SaveStateAsync ( "mystatestore" , "mykey" , widget );
}
catch ( DaprException ex )
{
// 处理异常,记录日志,重试等
}
最常见的故障情况将与以下内容相关:
Dapr 组件配置不正确 瞬时故障,例如网络问题 无效数据,例如 JSON 反序列化失败 在任何这些情况下,您都可以通过 .InnerException
属性检查更多异常详细信息。
2 - Dapr actors .NET SDK 快速掌握使用 Dapr actors .NET SDK 的方法
借助 Dapr actor 包,您可以在 .NET 应用程序中轻松与 Dapr 的虚拟 actor 进行交互。
要开始,请参阅 Dapr actors 指南。
2.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 的 类型 和 id 以便与其通信。对于强类型客户端,您还需要提供其接口之一。
actor 类型 在整个应用程序中唯一标识 actor 实现。actor id 唯一标识该类型的一个实例。如果您没有 actor id
并希望与新实例通信,可以使用 ActorId.CreateRandom()
创建一个随机 id。随机 id 是一个加密强标识符,运行时将在您与其交互时创建一个新的 actor 实例。
您可以使用 ActorReference
类型在消息中传递 actor 类型和 actor id,以便与其他 actor 进行交换。
两种风格的 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: 远程 actor 方法异常,详细信息:异常:NotImplementedException,方法名称:ExceptionExample,行号:14,异常 uuid:d291a006-84d5-42c4-b39e-d6300e9ac38b
下一步 了解如何使用 ActorHost
编写和运行 actor 。
2.2 - 编写和运行actor 了解如何使用.NET SDK编写和运行actor
编写actor ActorHost ActorHost
:
是所有actor构造函数所需的参数 由运行时提供的 必须传递给基类的构造函数 包含允许该actor实例与运行时通信的所有状态信息 internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host ) // 在构造函数中接收ActorHost
: base ( host ) // 将ActorHost传递给基类的构造函数
{
}
}
由于ActorHost
包含actor特有的状态信息,您不需要将其实例传递给代码的其他部分。建议仅在测试中创建您自己的ActorHost
实例。
依赖注入 actor支持通过依赖注入 将额外的参数传递到构造函数中。您定义的任何其他参数都将从依赖注入容器中获取其值。
internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host , BankService bank ) // 在构造函数中接收BankService
: base ( host )
{
...
}
}
一个actor类型应该只有一个public
构造函数。actor系统使用ActivatorUtilities
模式来创建actor实例。
您可以在Startup.cs
中注册类型以进行依赖注入以使其可用。阅读更多关于注册类型的不同方法 。
// 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
...
// 使用依赖注入注册额外的类型。
services . AddSingleton < BankService >();
}
每个actor实例都有其自己的依赖注入范围,并在执行操作后在内存中保留一段时间。在此期间,与actor关联的依赖注入范围也被视为活动状态。该范围将在actor被停用时释放。
如果actor在构造函数中注入IServiceProvider
,actor将接收到与其范围关联的IServiceProvider
的引用。IServiceProvider
可以用于将来动态解析服务。
internal class MyActor : Actor , IMyActor , IRemindable
{
public MyActor ( ActorHost host , IServiceProvider services ) // 在构造函数中接收IServiceProvider
: base ( host )
{
...
}
}
使用此模式时,避免创建许多实现IDisposable
的瞬态 服务。由于与actor关联的范围可能被视为有效时间较长,您可能会在内存中积累许多服务。有关更多信息,请参阅依赖注入指南 。
IDisposable和actor actor可以实现IDisposable
或IAsyncDisposable
。建议您依赖依赖注入进行资源管理,而不是在应用程序代码中实现释放功能。仅在确实必要的情况下提供释放支持。
日志记录 在actor类内部,您可以通过基类Actor
上的属性访问ILogger
实例。此实例连接到ASP.NET Core日志系统,应该用于actor内部的所有日志记录。阅读更多关于日志记录 。您可以配置各种不同的日志格式和输出接收器。
使用_结构化日志记录_和_命名占位符_,如下例所示:
public Task < MyData > GetDataAsync ()
{
this . Logger . LogInformation ( "获取状态时间为 {CurrentTime}" , DateTime . UtcNow );
return this . StateManager . GetStateAsync < MyData >( "my_data" );
}
记录日志时,避免使用格式字符串,如:$"获取状态时间为 {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
) // 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
// 使用DI注册actor运行时
services . AddActors ( options =>
{
// 注册actor类型并配置actor设置
options . Actors . RegisterActor < MyActor >();
// 配置默认设置
options . ActorIdleTimeout = TimeSpan . FromMinutes ( 10 );
options . ActorScanInterval = TimeSpan . FromSeconds ( 35 );
options . DrainOngoingCallTimeout = TimeSpan . FromSeconds ( 35 );
options . DrainRebalancedActors = true ;
});
// 注册额外的服务以供actor使用
services . AddSingleton < BankService >();
}
配置JSON选项 actor运行时使用System.Text.Json 进行:
默认情况下,actor运行时使用基于JsonSerializerDefaults.Web 的设置。
您可以在ConfigureServices
中配置JsonSerializerOptions
:
// 在Startup.cs中
public void ConfigureServices ( IServiceCollection services )
{
services . AddActors ( options =>
{
...
// 自定义JSON选项
options . JsonSerializerOptions = ...
});
}
actor和路由 ASP.NET Core对actor的托管支持使用端点路由 系统。.NET SDK不支持使用早期ASP.NET Core版本的传统路由系统托管actor。
由于actor使用端点路由,actor的HTTP处理程序是中间件管道的一部分。以下是设置包含actor的中间件管道的Configure
方法的最小示例。
// 在Startup.cs中
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
app . UseRouting ();
app . UseEndpoints ( endpoints =>
{
// 注册与Dapr运行时接口的actor处理程序。
endpoints . MapActorsHandlers ();
});
}
UseRouting
和UseEndpoints
调用是配置路由所必需的。通过在端点中间件中添加MapActorsHandlers
将actor配置为管道的一部分。
这是一个最小示例,actor功能可以与以下内容共存:
控制器 Razor页面 Blazor gRPC服务 Dapr pub/sub处理程序 其他端点,如健康检查 问题中间件 某些中间件可能会干扰Dapr请求到actor处理程序的路由。特别是,UseHttpsRedirection
对于Dapr的默认配置是有问题的。Dapr默认通过未加密的HTTP发送请求,这将被UseHttpsRedirection
中间件阻止。此中间件目前不能与Dapr一起使用。
// 在Startup.cs中
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
// 无效 - 这将阻止非HTTPS请求
app . UseHttpsRedirection ();
// 无效 - 这将阻止非HTTPS请求
app . UseRouting ();
app . UseEndpoints ( endpoints =>
{
// 注册与Dapr运行时接口的actor处理程序。
endpoints . MapActorsHandlers ();
});
}
下一步 尝试运行和使用虚拟actor示例 。
2.3 - .NET SDK 中的 Actor 序列化 使用 .NET 中的远程 Actor 序列化类型的必要步骤
Actor 序列化 Dapr actor 包使您能够在 .NET 应用程序中使用 Dapr 虚拟 actor,您可以选择使用弱类型或强类型客户端。每种方式都有不同的序列化方法。本文档将回顾这些差异,并传达一些在任一场景中需要理解的关键基本规则。
请注意,由于序列化方法的不同,弱类型和强类型 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
默认选项 ,除非通过自定义选项配置覆盖,这意味着将应用以下内容:
属性名称的反序列化以不区分大小写的方式进行 属性名称的序列化使用 驼峰命名法 ,除非属性被 [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 state 的类型,这不是必需的,因为您不打算独立于 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>
,因此您将枚举的名称保存为值而不是数字值,但如果您更改枚举名称,这将引入破坏性更改,因为名称将不再与 state 中的内容匹配。
请注意,如果您选择使用此方法,您应该为所有枚举成员装饰 [EnumMeber]
属性,以便为每个枚举值一致地应用值,而不是随意地。没有任何东西会在构建或运行时验证这一点,但这被认为是最佳实践操作。
在这种情况下,如何在仍然更改枚举成员名称的同时指定持久化的精确值?使用自定义 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]
属性中的值,为我们提供了一种机制来重构我们的应用程序,而无需为 state 中现有的枚举值制定复杂的版本控制方案。
{ "event" : "Conference" , "season" : "fall" }
强类型 Dapr Actor 客户端 在本节中,您将学习如何配置类和记录,以便在使用强类型 actor 客户端时,它们在运行时能够正确序列化和反序列化。这些客户端是使用 .NET 接口实现的,并且不 与使用其他语言编写的 Dapr actor 兼容。
此 actor 客户端使用称为 数据契约序列化器 的引擎序列化数据,该引擎将您的 C# 类型转换为 XML 文档。此序列化框架并不是 Dapr 特有的,而是由 .NET 团队在 .NET GitHub 仓库 中单独维护的。
在发送或接收原始类型(如字符串或整数)时,此序列化会透明地进行,您无需进行任何准备。然而,当处理您创建的复杂类型时,有一些重要规则需要考虑,以便此过程顺利进行。
可序列化类型 使用数据契约序列化器时需要牢记几个重要注意事项:
默认情况下,所有类型、读/写属性(构造后)和标记为公开可见的字段都会被序列化 所有类型必须公开一个无参数构造函数或用 DataContractAttribute 属性装饰 仅在使用 DataContractAttribute 属性时支持仅初始化的设置器 只读字段、没有 Get 和 Set 方法的属性以及具有私有 Get 和 Set 方法的内部或属性在序列化期间会被忽略 通过使用 KnownTypesAttribute 属性,支持使用其他复杂类型的类型的序列化,这些复杂类型本身未标记为 DataContractAttribute 属性 如果类型标记为 DataContractAttribute 属性,则您希望序列化和反序列化的所有成员也必须用 DataMemberAttribute 属性装饰,否则它们将被设置为默认值 反序列化如何工作? 反序列化使用的方法取决于类型是否用 DataContractAttribute 属性装饰。如果没有此属性,则使用无参数构造函数创建类型的实例。然后使用各自的设置器将每个属性和字段映射到类型中,并将实例返回给调用者。
如果类型标记为 [DataContract]
,则序列化器会使用反射读取类型的元数据,并根据它们是否标记为 DataMemberAttribute 属性来确定应包含哪些属性或字段,因为这是基于选择加入的。然后在内存中分配一个未初始化的对象(避免使用任何构造函数,无论是否有参数),然后直接在每个映射的属性或字段上设置值,即使是私有的或使用仅初始化的设置器。在整个过程中会根据需要调用序列化回调,然后将对象返回给调用者。
强烈建议使用序列化属性,因为它们提供了更多灵活性来覆盖名称和命名空间,并且通常使用更多现代 C# 功能。虽然默认序列化器可以依赖于原始类型,但不建议用于您自己的任何类型,无论它们是类、结构还是记录。建议如果您用 DataContractAttribute 属性装饰类型,还要显式装饰您希望序列化或反序列化的每个成员的 DataMemberAttribute 属性。
.NET 类 只要遵循本页和 数据契约序列化器 文档中详细说明的其他规则,类在数据契约序列化器中是完全支持的。
这里最重要的是要记住,您必须要么有一个公共无参数构造函数,要么用适当的属性装饰它。让我们通过一些示例来真正澄清什么会起作用,什么不会。
在以下示例中,我们展示了一个名为 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>
所以让我们调整一下——让我们添加我们自己的构造函数,并仅在成员上使用仅初始化的设置器。这将无法正确序列化和反序列化,不是因为使用了仅初始化的设置器,而是因为缺少无参数构造函数。
// 无法正确序列化!
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 属性装饰了类型,这意味着我可以在属性上使用仅初始化的设置器。
[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 ; }
}
以及使用我们的序列化属性(再次选择仅初始化的设置器,因为我们使用的是序列化属性):
[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 结构体 只要它们标记为 DataContractAttribute 属性,并且您希望序列化的成员标记为 DataMemberAttribute 属性,结构体就可以被数据契约序列化器支持。此外,为了支持反序列化,结构体还需要有一个无参数构造函数。即使您定义了自己的无参数构造函数(在 C# 10 中启用),这也能正常工作。
[DataContract]
public struct Doodad
{
[DataMember]
public int Count { get ; set ; }
}
.NET 记录 记录是在 C# 9 中引入的,在序列化方面遵循与类完全相同的规则。我们建议您应该用 DataContractAttribute 属性装饰所有记录,并用 DataMemberAttribute 属性装饰您希望序列化的成员,以便在使用此或其他较新的 C# 功能时不会遇到反序列化问题。因为记录类默认使用仅初始化的设置器来设置属性,并鼓励使用主构造函数,所以将这些属性应用于您的类型可以确保序列化器能够正确处理您的类型。
通常,记录以使用新主构造函数概念的简单单行语句呈现:
public record Doodad ( Guid Id , string Name , int Count );
这将抛出一个错误,鼓励使用序列化属性,因为在 Dapr actor 方法调用中使用它时没有可用的无参数构造函数,也没有用上述属性装饰。
在这里,我们添加了一个显式的无参数构造函数,它不会抛出错误,但在反序列化期间不会设置任何值,因为它们是使用仅初始化的设置器创建的。因为这没有使用 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/en-us/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
实例时,它将不知道如何处理派生类型,这可能导致序列化异常。通过将所有可能的派生类型指定为已知类型,您可以确保序列化器能够正确处理类型及其成员。
有关使用 [KnownType]
的更多信息和示例,请参阅官方文档 。
2.4 - 如何:在 .NET SDK 中运行和使用虚拟 actor 通过此示例尝试 .NET Dapr 虚拟 actor
Dapr actor 包使您能够从 .NET 应用程序中与 Dapr 虚拟 actor 交互。在本指南中,您将学习如何:
创建一个 actor (MyActor
)。 在客户端应用程序上调用其方法。 MyActor --- MyActor.Interfaces
|
+- MyActorService
|
+- MyActorClient
接口项目 (\MyActor\MyActor.Interfaces)
此项目包含 actor 的接口定义。actor 接口可以在任何项目中定义,名称不限。接口定义了 actor 实现和调用 actor 的客户端共享的 actor 合约:
由于客户端项目可能依赖于它,最好将其定义在与 actor 实现分开的程序集内。
actor 服务项目 (\MyActor\MyActorService)
此项目实现了托管 actor 的 ASP.Net Core Web 服务。它包含 actor 的实现,MyActor.cs
。actor 实现是一个类,它:
派生自基础类型 actor 实现 MyActor.Interfaces
项目中定义的接口。 actor 类还必须实现一个构造函数,该构造函数接受一个 ActorService
实例和一个 ActorId
,并将它们传递给基础 actor 类。
actor 客户端项目 (\MyActor\MyActorClient)
此项目包含 actor 客户端的实现,该客户端调用在 actor 接口中定义的 MyActor 的方法。
准备工作
注意 请注意,虽然 .NET 6 通常作为 Dapr .NET SDK 包的最低 .NET 要求得到支持,而 .NET 7 是 Dapr v1.15 中 Dapr.Workflows 的最低支持版本,但只有 .NET 8 和 .NET 9 将继续在 v1.16 及更高版本中得到 Dapr 的支持。步骤 0:准备 我们将创建 3 个项目,请选择一个空目录开始,并在您选择的终端中打开它。
步骤 1:创建 actor 接口 actor 接口定义了 actor 实现和调用 actor 的客户端共享的 actor 合约。
actor 接口定义如下要求:
actor 接口必须继承 Dapr.Actors.IActor
接口 actor 方法的返回类型必须是 Task
或 Task<object>
actor 方法最多可以有一个参数 创建接口项目并添加依赖项 # 创建 actor 接口
dotnet new classlib -o MyActor.Interfaces
cd MyActor.Interfaces
# 添加 Dapr.Actors nuget 包。请使用 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 运行时。
创建 actor 服务项目并添加依赖项 # 创建 ASP.Net Web 服务以托管 Dapr actor
dotnet new web -o MyActorService
cd MyActorService
# 添加 Dapr.Actors.AspNetCore nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors.AspNetCore
# 添加 actor 接口引用
dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
cd ..
添加 actor 实现 实现 IMyActor 接口并从 Dapr.Actors.Actor
类派生。以下示例还展示了如何使用 actor reminder。对于使用 reminder 的 actor,它必须从 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
{
// 构造函数必须接受 ActorHost 作为参数,并且还可以接受将从依赖注入容器中检索的其他参数
//
/// <summary>
/// 初始化 MyActor 的新实例
/// </summary>
/// <param name="host">将托管此 actor 实例的 Dapr.Actors.Runtime.ActorHost。</param>
public MyActor ( ActorHost host )
: base ( host )
{
}
/// <summary>
/// 每当 actor 被激活时调用此方法。
/// actor 在其任何方法首次被调用时被激活。
/// </summary>
protected override Task OnActivateAsync ()
{
// 提供执行一些可选设置的机会。
Console . WriteLine ( $"Activating actor id: {this.Id}" );
return Task . CompletedTask ;
}
/// <summary>
/// 每当 actor 在一段时间不活动后被停用时调用此方法。
/// </summary>
protected override Task OnDeactivateAsync ()
{
// 提供执行可选清理的机会。
Console . WriteLine ( $"Deactivating actor id: {this.Id}" );
return Task . CompletedTask ;
}
/// <summary>
/// 将 MyData 设置到 actor 的私有状态存储中
/// </summary>
/// <param name="data">用户定义的 MyData,将作为 "my_data" 状态存储到状态存储中</param>
public async Task < string > SetDataAsync ( MyData data )
{
// 数据在每次方法执行后由 actor 的运行时隐式保存到配置的状态存储中。
// 数据也可以通过调用 this.StateManager.SaveStateAsync() 显式保存。
// 要保存的状态必须是 DataContract 可序列化的。
await this . StateManager . SetStateAsync < MyData >(
"my_data" , // 状态名称
data ); // 为命名状态 "my_data" 保存的数据
return "Success" ;
}
/// <summary>
/// 从 actor 的私有状态存储中获取 MyData
/// </summary>
/// <return>存储到状态存储中的用户定义的 MyData,作为 "my_data" 状态</return>
public Task < MyData > GetDataAsync ()
{
// 从状态存储中获取状态。
return this . StateManager . GetStateAsync < MyData >( "my_data" );
}
/// <summary>
/// 向 actor 注册 MyReminder reminder
/// </summary>
public async Task RegisterReminder ()
{
await this . RegisterReminderAsync (
"MyReminder" , // reminder 的名称
null , // 传递给 IRemindable.ReceiveReminderAsync() 的用户状态
TimeSpan . FromSeconds ( 5 ), // 在首次调用 reminder 之前的延迟时间
TimeSpan . FromSeconds ( 5 )); // 在首次调用后 reminder 调用之间的时间间隔
}
/// <summary>
/// 获取 actor 的 MyReminder reminder 详细信息
/// </summary>
public async Task < IActorReminder > GetReminder ()
{
await this . GetReminderAsync ( "MyReminder" );
}
/// <summary>
/// 取消注册 actor 的 MyReminder reminder
/// </summary>
public Task UnregisterReminder ()
{
Console . WriteLine ( "Unregistering MyReminder..." );
return this . UnregisterReminderAsync ( "MyReminder" );
}
// <summary>
// 实现 IRemindeable.ReceiveReminderAsync(),这是在 actor reminder 触发时调用的回调。
// </summary>
public Task ReceiveReminderAsync ( string reminderName , byte [] state , TimeSpan dueTime , TimeSpan period )
{
Console . WriteLine ( "ReceiveReminderAsync is called!" );
return Task . CompletedTask ;
}
/// <summary>
/// 向 actor 注册 MyTimer timer
/// </summary>
public Task RegisterTimer ()
{
return this . RegisterTimerAsync (
"MyTimer" , // timer 的名称
nameof ( this . OnTimerCallBack ), // timer 回调
null , // 传递给 OnTimerCallback() 的用户状态
TimeSpan . FromSeconds ( 5 ), // 在首次调用异步回调之前的延迟时间
TimeSpan . FromSeconds ( 5 )); // 异步回调调用之间的时间间隔
}
/// <summary>
/// 取消注册 actor 的 MyTimer timer
/// </summary>
public Task UnregisterTimer ()
{
Console . WriteLine ( "Unregistering MyTimer..." );
return this . UnregisterTimerAsync ( "MyTimer" );
}
/// <summary>
/// timer 到期后调用的回调
/// </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 类型的构造函数中。
actor 是通过与 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 =>
{
// 注册 actor 类型并配置 actor 设置
options . Actors . RegisterActor < MyActor >();
});
}
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
}
app . UseRouting ();
// 注册与 Dapr 运行时接口的 actor 处理程序。
app . MapActorsHandlers ();
}
}
}
步骤 3:添加客户端 创建一个简单的控制台应用程序来调用 actor 服务。Dapr SDK 提供 actor 代理客户端来调用 actor 接口中定义的 actor 方法。
创建 actor 客户端项目并添加依赖项 # 创建 actor 的客户端
dotnet new console -o MyActorClient
cd MyActorClient
# 添加 Dapr.Actors nuget 包。请使用 nuget.org 上的最新包版本
dotnet add package Dapr.Actors
# 添加 actor 接口引用
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..." );
// 在 actor 服务中注册的 actor 类型
var actorType = "MyActor" ;
// ActorId 唯一标识一个 actor 实例
// 如果与此 id 匹配的 actor 不存在,将会创建它
var actorId = new ActorId ( "1" );
// 使用服务实现的相同接口创建本地代理。
//
// 您需要提供类型和 id,以便可以定位 actor。
var proxy = ActorProxy . Create < IMyActor >( actorId , actorType );
// 现在您可以使用 actor 接口调用 actor 的方法。
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
托管 actor,因此需要使用 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,这被传递给 dapr run
作为 --app-port 5000
。Dapr sidecar 的默认 HTTP 端口是 3500。我们告诉 MyActorService
的 sidecar 使用 3500,以便 MyActorClient
可以依赖默认值。
现在您已成功创建了一个 actor 服务和客户端。请参阅相关链接部分以了解更多信息。
相关链接 3 - Dapr Workflow .NET SDK 快速上手并掌握 Dapr Workflow 和 Dapr .NET SDK 的使用
3.1 - DaprWorkflowClient 使用 使用 DaprWorkflowClient 的基本提示和建议
生命周期管理 DaprWorkflowClient
可以访问网络资源,这些资源通过 TCP 套接字与 Dapr sidecar 以及其他用于管理和操作工作流的类型进行通信。DaprWorkflowClient
实现了 IAsyncDisposable
接口,以便快速清理资源。
依赖注入 AddDaprWorkflow()
方法用于通过 ASP.NET Core 的依赖注入机制注册 Dapr 工作流服务。此方法需要一个选项委托,用于定义您希望在应用程序中注册和使用的每个工作流和活动。
注意 此方法会尝试注册一个 DaprClient
实例,但仅在尚未以其他生命周期注册的情况下才有效。例如,如果之前以单例生命周期调用了 AddDaprClient()
,那么无论为工作流客户端选择何种生命周期,都会始终使用单例。DaprClient
实例用于与 Dapr sidecar 通信,如果尚未注册,则在 AddDaprWorkflow()
注册期间提供的生命周期将用于注册 DaprWorkflowClient
及其依赖项。单例注册 默认情况下,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 ();
将服务注入到工作流活动中 工作流活动支持现代 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 );
}
}
在工作流中使用 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 );
//...
}
}
3.2 - 如何:在 .NET SDK 中编写和管理 Dapr 工作流 学习如何使用 .NET SDK 编写和管理 Dapr 工作流
我们来创建一个 Dapr 工作流并通过控制台调用它。在提供的订单处理工作流示例 中,控制台会提示如何进行购买和补货。在本指南中,您将:
在 .NET 示例项目里:
先决条件
注意 Dapr.Workflows 在 v1.15 中支持 .NET 7 或更高版本。然而,从 Dapr v1.16 开始,仅支持 .NET 8 和 .NET 9。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 工作流示例。
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 WorkflowConsoleApp
目录。
启动程序。
在一个新的终端中,再次导航到 WorkflowConsoleApp
目录,并在程序旁边运行 Dapr sidecar。
dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500
Dapr 会监听 HTTP 请求在 http://localhost:3500
和内部工作流 gRPC 请求在 http://localhost:4001
。
启动工作流 要启动工作流,您有两种选择:
按照控制台提示的指示。 使用工作流 API 并直接向 Dapr 发送请求。 本指南重点介绍工作流 API 选项。
注意 您可以在 WorkflowConsoleApp
/demo.http
文件中找到以下命令。 curl 请求的主体是作为工作流输入的采购订单信息。 命令中的 “12345678” 表示工作流的唯一标识符,可以替换为您选择的任何标识符。 运行以下命令以启动工作流。
curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID= 12345678 \
-H "Content-Type: application/json" \
-d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'
curl -i -X POST http : // localhost : 3500 / v1 . 0 / workflows / dapr / OrderProcessingWorkflow / start ? instanceID = 12345678 `
-H "Content-Type: application/json" `
-d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}'
如果成功,您应该会看到如下响应:
{ "instanceID" : "12345678" }
发送 HTTP 请求以获取已启动工作流的状态:
curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/12345678
工作流设计为需要几秒钟才能完成。如果在您发出 HTTP 请求时工作流尚未完成,您将看到以下 JSON 响应(为便于阅读而格式化),工作流状态为 RUNNING
:
{
"instanceID" : "12345678" ,
"workflowName" : "OrderProcessingWorkflow" ,
"createdAt" : "2023-05-10T00:42:03.911444105Z" ,
"lastUpdatedAt" : "2023-05-10T00:42:06.142214153Z" ,
"runtimeStatus" : "RUNNING" ,
"properties" : {
"dapr.workflow.custom_status" : "" ,
"dapr.workflow.input" : "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}"
}
}
一旦工作流完成运行,您应该会看到以下输出,表明它已达到 COMPLETED
状态:
{
"instanceID" : "12345678" ,
"workflowName" : "OrderProcessingWorkflow" ,
"createdAt" : "2023-05-10T00:42:03.911444105Z" ,
"lastUpdatedAt" : "2023-05-10T00:42:18.527704176Z" ,
"runtimeStatus" : "COMPLETED" ,
"properties" : {
"dapr.workflow.custom_status" : "" ,
"dapr.workflow.input" : "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" ,
"dapr.workflow.output" : "{\"Processed\":true}"
}
}
当工作流完成时,工作流应用程序的标准输出应如下所示:
info: WorkflowConsoleApp.Activities.NotifyActivity[0]
Received order 12345678 for Paperclips at $99.95
info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0]
Reserving inventory: 12345678, Paperclips, 1
info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0]
Processing payment: 12345678, 99.95, USD
info: WorkflowConsoleApp.Activities.NotifyActivity[0]
Order 12345678 processed successfully!
如果您在本地机器上为 Dapr 配置了 Zipkin,那么您可以在 Zipkin Web UI(通常在 http://localhost:9411/zipkin/)中查看工作流跟踪跨度。
演示 观看此视频演示 .NET 工作流 :
下一步 4 - Dapr AI .NET SDK 快速上手使用 Dapr AI .NET SDK
使用 Dapr AI 包,您可以从 .NET 应用程序与 Dapr AI 工作负载进行交互。
目前,Dapr 提供了一个会话 API,用于与大型语言模型进行交互。要开始使用此功能,请参阅 Dapr 会话 AI 指南。
4.1 - Dapr AI 客户端 学习如何创建 Dapr AI 客户端
Dapr AI 客户端包使您能够与 Dapr sidecar 提供的 AI 功能进行交互。
生命周期的管理 DaprConversationClient
是专门用于与 Dapr conversation API 交互的客户端版本。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,以便访问网络资源。
为了获得最佳性能,建议创建一个长期存在的 DaprConversationClient
实例,并在整个应用程序中共享使用。DaprConversationClient
实例是线程安全的,适合共享。
这可以通过依赖注入来实现。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以利用 IConfiguration
或其他注入服务中的值进行注册,这在每个类中从头创建客户端时是不切实际的。
避免为每个操作都创建一个新的 DaprConversationClient
。
通过 DaprConversationClientBuilder 配置 DaprConversationClient 可以通过在 DaprConversationClientBuilder
类上调用方法来配置 DaprConversationClient
,然后调用 .Build()
来创建客户端。每个 DaprConversationClient
的设置是独立的,并且在调用 .Build()
后无法更改。
var daprConversationClient = new DaprConversationClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于验证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprConversationClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于验证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 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 ();
有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 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 );
// 注册一个虚构的服务,从某处检索 secret
builder . Services . AddSingleton < SecretService >();
builder . Services . AddDaprConversationClient (( serviceProvider , daprConversationClientBuilder ) => {
// 从服务提供者中检索 `SecretService` 的实例
var secretService = serviceProvider . GetRequiredService < SecretService >();
var daprApiToken = secretService . GetSecret ( "DaprApiToken" ). Value ;
// 配置 `DaprConversationClientBuilder`
daprConversationClientBuilder . UseDaprApiToken ( daprApiToken );
});
var app = builder . Build ();
4.2 - 如何在 .NET SDK 中创建和使用 Dapr AI 会话 学习如何使用 .NET SDK 创建和使用 Dapr 会话 AI 客户端
前提条件
注意事项 .NET 6 是此版本中 Dapr .NET SDK 包的最低支持版本。仅 .NET 8 和 .NET 9 将在 Dapr v1.16 及更高版本中得到支持。安装 要开始使用 Dapr AI .NET SDK 客户端,请从 NuGet 安装 Dapr.AI 包 :
dotnet add package Dapr.AI
DaprConversationClient
通过 TCP 套接字形式维护对网络资源的访问,用于与 Dapr sidecar 通信。
依赖注入 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 接口,以便从大型语言模型发送和接收消息。
发送消息 5 - Dapr Jobs .NET SDK 快速上手使用 Dapr Jobs 和 Dapr .NET SDK
使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 进行交互。通过预设的计划,您可以安排未来的操作,并可选择附带数据。
要开始使用,请查看 Dapr Jobs 指南,并参考 最佳实践文档 以获取更多指导。
5.1 - 如何:在 .NET SDK 中编写和管理 Dapr 任务 学习如何使用 .NET SDK 编写和管理 Dapr 任务
我们来创建一个端点,该端点将在 Dapr 任务触发时被调用,然后在同一个应用中调度该任务。我们将使用此处提供的简单示例 ,进行以下演示,并通过它来解释如何使用间隔或 Cron 表达式自行调度一次性或重复性任务。在本指南中,您将:
部署一个 .NET Web API 应用程序 (JobsSample ) 利用 Dapr .NET 任务 SDK 调度任务调用并设置被触发的端点 在 .NET 示例项目中:
前提条件
注意 请注意,虽然 .NET 6 是 Dapr v1.15 中支持的最低版本,但从 v1.16 开始,Dapr 仅支持 .NET 8 和 .NET 9。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 任务示例。
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 JobsSample
目录。
我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。
dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 监听 HTTP 请求在 http://localhost:3500
和内部任务 gRPC 请求在 http://localhost:4001
。
使用依赖注入注册 Dapr 任务客户端 Dapr 任务 SDK 提供了一个扩展方法来简化 Dapr 任务客户端的注册。在 Program.cs
中完成依赖注入注册之前,添加以下行:
var builder = WebApplication . CreateBuilder ( args );
//在这两行之间的任意位置添加
builder . Services . AddDaprJobsClient (); //这样就完成了
var app = builder . Build ();
请注意,在当前的任务 API 实现中,调度任务的应用也将是接收触发通知的应用。换句话说,您不能调度一个触发器在另一个应用中运行。因此,虽然您不需要在应用中显式注册 Dapr 任务客户端来调度触发调用端点,但如果没有同一个应用以某种方式调度任务(无论是通过此 Dapr 任务 .NET SDK 还是对 sidecar 的 HTTP 调用),您的端点将永远不会被调用。
您可能希望为 Dapr 任务客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprJobsClientBuilder
实例的注册方法重载来实现:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (( _ , daprJobsClientBuilder ) =>
{
daprJobsClientBuilder . UseDaprApiToken ( "abc123" );
daprJobsClientBuilder . UseHttpEndpoint ( "http://localhost:8512" ); //非标准 sidecar HTTP 端点
});
var app = builder . Build ();
如果您需要从其他来源检索注入的值,这些来源本身注册为依赖项,您可以使用另一个重载来将 IServiceProvider
注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret,并将其传递到 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 任务客户端 可以使用注册的 IConfiguration
中的值来配置 Dapr 任务客户端,而无需显式指定每个值的重写,如前一节中使用 DaprJobsClientBuilder
所示。相反,通过填充通过依赖注入提供的 IConfiguration
,AddDaprJobsClient()
注册将自动使用这些值覆盖其各自的默认值。
首先在您的配置中填充值。这可以通过以下示例中的几种不同方式完成。
通过 ConfigurationBuilder
配置 应用程序设置可以在不使用配置源的情况下配置,而是通过填充 ConfigurationBuilder
实例中的值来实现:
var builder = WebApplication . CreateBuilder ();
//创建配置
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 任务客户端的 HTTP 端点和 API 令牌。
Key Value DAPR_HTTP_ENDPOINT http://localhost:54321 DAPR_API_TOKEN abc123
var builder = WebApplication . CreateBuilder ();
builder . Configuration . AddEnvironmentVariables ();
builder . Services . AddDaprJobsClient ();
Dapr 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
通过前缀环境变量配置 然而,在共享主机场景中,多个应用程序都在同一台机器上运行而不使用容器或在开发环境中,前缀环境变量并不罕见。以下示例假设 HTTP 端点和 API 令牌都将从前缀为 “myapp_” 的环境变量中提取。在此场景中使用的两个环境变量如下:
Key Value 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 任务客户端将被配置为使用 HTTP 端点 http://localhost:54321
并用 API 令牌头 abc123
填充所有出站请求。
不依赖于依赖注入使用 Dapr 任务客户端 虽然使用依赖注入简化了 .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 请求映射的方式配置应用程序。实现为扩展方法,传递它应该响应的任务名称和一个委托。服务可以根据需要注入到委托的参数中,您可以选择传递 JobDetails
以获取有关已触发任务的信息(例如,访问其调度设置或负载)。
这里有两个委托可以使用。一个提供 IServiceProvider
以防您需要将其他服务注入处理程序:
//我们从上面的示例中得到了这个
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient ();
var app = builder . Build ();
//添加我们的端点注册
app . MapDaprScheduledJob ( "myJob" , ( IServiceProvider serviceProvider , string? jobName , JobDetails ? jobDetails ) => {
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 , JobDetails ? jobDetails ) => {
//做一些事情...
});
app . Run ();
注册任务 最后,我们必须注册我们想要调度的任务。请注意,从这里开始,所有 SDK 方法都支持取消令牌,并在未设置时使用默认令牌。
有三种不同的方式来设置任务,具体取决于您想要如何配置调度:
一次性任务 一次性任务就是这样;它将在某个时间点运行,并且不会重复。这种方法要求您选择一个任务名称并指定一个触发时间。
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 scheduledTime DateTime 任务应运行的时间点。 是 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度一次性任务,如以下示例所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleOneTimeJobAsync ( CancellationToken cancellationToken )
{
var today = DateTime . UtcNow ;
var threeDaysFromNow = today . AddDays ( 3 );
await daprJobsClient . ScheduleOneTimeJobAsync ( "myJobName" , threeDaysFromNow , cancellationToken : cancellationToken );
}
}
基于间隔的任务 基于间隔的任务是一个在配置为固定时间量的循环中运行的任务,不像今天在 actor 构建块中工作的提醒 。这些任务也可以通过许多可选参数进行调度:
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 interval TimeSpan 任务应触发的间隔。 是 startingFrom DateTime 任务调度应开始的时间点。 否 repeats int 任务应触发的最大次数。 否 ttl 任务何时过期且不再触发。 否 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度基于间隔的任务,如以下示例所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleIntervalJobAsync ( CancellationToken cancellationToken )
{
var hourlyInterval = TimeSpan . FromHours ( 1 );
//每小时触发任务,但最多触发 5 次
await daprJobsClient . ScheduleIntervalJobAsync ( "myJobName" , hourlyInterval , repeats : 5 ), cancellationToken : cancellationToken ;
}
}
基于 Cron 的任务 基于 Cron 的任务是使用 Cron 表达式调度的。这提供了更多基于日历的控制,以便在任务触发时使用日历值在表达式中。与其他选项一样,这些任务也可以通过许多可选参数进行调度:
参数名称 类型 描述 必需 jobName string 正在调度的任务的名称。 是 cronExpression string 指示任务应触发的 systemd 类似 Cron 表达式。 是 startingFrom DateTime 任务调度应开始的时间点。 否 repeats int 任务应触发的最大次数。 否 ttl 任务何时过期且不再触发。 否 payload ReadOnlyMemory 触发时提供给调用端点的任务数据。 否 cancellationToken CancellationToken 用于提前取消操作,例如由于操作超时。 否
可以从 Dapr 任务客户端调度基于 Cron 的任务,如下所示:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task ScheduleCronJobAsync ( CancellationToken cancellationToken )
{
//在每个月的第五天的每隔一小时的顶部
const string cronSchedule = "0 */2 5 * *" ;
//直到下个月才开始
var now = DateTime . UtcNow ;
var oneMonthFromNow = now . AddMonths ( 1 );
var firstOfNextMonth = new DateTime ( oneMonthFromNow . Year , oneMonthFromNow . Month , 1 , 0 , 0 , 0 );
//每小时触发任务,但最多触发 5 次
await daprJobsClient . ScheduleCronJobAsync ( "myJobName" , cronSchedule , dueTime : firstOfNextMonth , cancellationToken : cancellationToken );
}
}
获取已调度任务的详细信息 如果您知道已调度任务的名称,您可以在不等待其触发的情况下检索其元数据。返回的 JobDetails
提供了一些有用的属性,用于从 Dapr 任务 API 消费信息:
如果 Schedule
属性包含 Cron 表达式,则 IsCronExpression
属性将为 true,并且表达式也将在 CronExpression
属性中可用。 如果 Schedule
属性包含持续时间值,则 IsIntervalExpression
属性将为 true,并且该值将转换为 TimeSpan
值,可从 Interval
属性访问。 这可以通过使用以下方法完成:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task < JobDetails > GetJobDetailsAsync ( string jobName , CancellationToken cancellationToken )
{
var jobDetails = await daprJobsClient . GetJobAsync ( jobName , canecllationToken );
return jobDetails ;
}
}
删除已调度的任务 要删除已调度的任务,您需要知道其名称。从那里开始,只需在 Dapr 任务客户端上调用 DeleteJobAsync
方法即可:
public class MyOperation ( DaprJobsClient daprJobsClient )
{
public async Task DeleteJobAsync ( string jobName , CancellationToken cancellationToken )
{
await daprJobsClient . DeleteJobAsync ( jobName , cancellationToken );
}
}
5.2 - DaprJobsClient 使用指南 使用 DaprJobsClient 的基本技巧和建议
生命周期管理 DaprJobsClient
是专门用于与 Dapr Jobs API 交互的 Dapr 客户端。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,并实现了 IDisposable
接口以便快速清理资源。
为了获得最佳性能,建议创建一个长生命周期的 DaprJobsClient
实例,并在整个应用程序中共享使用。DaprJobsClient
实例是线程安全的,适合在多个线程中共享。
可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态方式注册,但也可以利用 IConfiguration
或其他注入服务中的值进行注册,这样就不需要在每个类中重新创建客户端。
避免为每个操作创建一个新的 DaprJobsClient
并在操作完成后销毁它。
通过 DaprJobsClientBuilder 配置 DaprJobsClient 可以通过 DaprJobsClientBuilder
类上的方法来配置 DaprJobsClient
,然后调用 .Build()
来创建客户端。每个 DaprJobsClient
的设置是独立的,调用 .Build()
后无法更改。
var daprJobsClient = new DaprJobsClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于验证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprJobsClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于验证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 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
(如果尚未注册),并在创建 HttpClient
实例时配置 DaprJobsClientBuilder
以尽可能多地重用相同的实例,避免套接字耗尽和其他问题。
在第一种方法中,开发人员没有进行任何配置,DaprJobsClient
使用默认设置进行配置。
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprJobsClient (); //根据需要注册 `DaprJobsClient` 以进行注入
var app = builder . Build ();
有时,开发人员需要使用上面详细介绍的各种配置选项来配置创建的客户端。这是通过传入 DaprJobsClientBuiler
并公开用于配置必要选项的方法的重载来完成的。
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 );
//注册一个虚构的服务,从某处检索 secret
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 编码字节的数组。以下是调度一次性 job 时可能的示例:
var now = DateTime . UtcNow ;
var oneWeekFromNow = now . AddDays ( 7 );
await daprJobsClient . ScheduleOneTimeJobWithPayloadAsync ( "myOtherJob" , oneWeekFromNow , "This is a test!" );
JobDetails
类型将数据返回为 ReadOnlyMemory<byte>?
,因此开发人员可以根据需要进行反序列化,但同样有两个包含的辅助扩展,可以将其反序列化为 JSON 兼容类型或字符串。这两种方法都假设开发人员对最初调度的 job 进行了编码(可能使用辅助序列化方法),因为这些方法不会强制字节表示它们不是的东西。
要将字节反序列化为字符串,可以使用以下辅助方法:
if ( jobDetails . Payload is not null )
{
string payloadAsString = jobDetails . Payload . DeserializeToString (); //如果成功,返回一个具有值的字符串
}
要将 JSON 编码的 UTF-8 字节反序列化为相应的类型,可以使用以下辅助方法。提供了一个重载参数,允许开发人员传入自己的 JsonSerializerOptions
以在反序列化期间应用。
public sealed record Doodad ( string Name , int Value );
//...
if ( jobDetails . Payload is not null )
{
var deserializedDoodad = jobDetails . Payload . DeserializeFromJsonBytes < Doodad >();
}
错误处理 DaprJobsClient
上的方法如果在 SDK 和运行在 Dapr sidecar 上的 Jobs API 服务之间遇到问题,将抛出 DaprJobsServiceException
。如果由于通过此 SDK 向 Jobs API 服务发出的请求格式不正确而遇到失败,将抛出 DaprMalformedJobException
。在非法参数值的情况下,将抛出适当的标准异常(例如 ArgumentOutOfRangeException
或 ArgumentNullException
),并附上有问题的参数名称。对于其他任何情况,将抛出 DaprException
。
最常见的失败情况将与以下内容相关:
在与 Jobs API 交互时参数格式不正确 瞬态故障,例如网络问题 无效数据,例如无法将值反序列化为其最初未从中序列化的类型 在任何这些情况下,您都可以通过 .InnerException
属性检查更多异常详细信息。
6 - Dapr Messaging .NET SDK 快速上手使用 Dapr Messaging .NET SDK
使用 Dapr Messaging 包,您可以在 .NET 应用程序中与 Dapr 消息 API 进行交互。在 v1.15 版本中,该包仅支持流式 pubsub 功能 。
未来的 Dapr .NET SDK 版本将会把现有的消息功能从 Dapr.Client 迁移到 Dapr.Messaging 包中。这一变更将在发布说明、文档和相关的技术说明中提前告知。
要开始使用,请查看 Dapr Messaging 指南,并参考最佳实践文档 以获取更多指导。
6.1 - 如何:在 .NET SDK 中编写和管理 Dapr 流式订阅 学习如何使用 .NET SDK 编写和管理 Dapr 流式订阅
我们来创建一个使用流式功能的发布/订阅主题或队列的订阅。我们将使用此处提供的简单示例 ,进行演示,并逐步讲解如何在运行时配置消息处理程序,而无需预先配置端点。在本指南中,您将会学习如何:
前提条件
注意 请注意,虽然 .NET 6 是 Dapr v1.15 中支持的最低版本,但只有 .NET 8 和 .NET 9 将在 v1.16 及更高版本中继续受到支持。设置环境 克隆 .NET SDK 仓库 。
git clone https://github.com/dapr/dotnet-sdk.git
从 .NET SDK 根目录,导航到 Dapr 流式发布/订阅示例。
cd examples/Client/PublishSubscribe
本地运行应用程序 要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 StreamingSubscriptionExample
目录。
cd StreamingSubscriptionExample
我们将运行一个命令,同时启动 Dapr sidecar 和 .NET 程序。
dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr 监听 HTTP 请求在 http://localhost:3500
,而 gRPC 请求在 http://localhost:4001
。
使用依赖注入注册 Dapr PubSub 客户端 Dapr Messaging SDK 提供了一个扩展方法来简化 Dapr PubSub 客户端的注册。在 Program.cs
中完成依赖注入注册之前,添加以下行:
var builder = WebApplication . CreateBuilder ( args );
//可以在这两行之间的任何位置添加
builder . Services . AddDaprPubSubClient (); //就是这样
var app = builder . Build ();
您可能希望为 Dapr PubSub 客户端提供一些配置选项,这些选项应在每次调用 sidecar 时存在,例如 Dapr API 令牌,或者您希望使用非标准的 HTTP 或 gRPC 端点。这可以通过使用允许配置 DaprPublishSubscribeClientBuilder
实例的注册方法重载来实现:
var builder = WebApplication . CreateBuilder ( args );
builder . Services . AddDaprPubSubClient (( _ , daprPubSubClientBuilder ) => {
daprPubSubClientBuilder . UseDaprApiToken ( "abc123" );
daprPubSubClientBuilder . UseHttpEndpoint ( "http://localhost:8512" ); //非标准 sidecar HTTP 端点
});
var app = builder . Build ();
尽管如此,您可能希望注入的任何值需要从其他来源检索,该来源本身注册为依赖项。您可以使用另一个重载将 IServiceProvider
注入到配置操作方法中。在以下示例中,我们注册了一个虚构的单例,可以从某处检索 secret 并将其传递到 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 ();
//创建配置
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 (); //这将自动从 IConfiguration 填充 HTTP 端点和 API 令牌值
通过环境变量配置 应用程序设置可以从可用于您的应用程序的环境变量中访问。
以下环境变量将用于填充用于注册 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 ();
//使用 `daprPubSubClient` 做一些事情
}
}
设置消息处理程序 Dapr 中的流式订阅实现使您可以更好地控制事件的背压处理,通过在您的应用程序准备好接受它们之前将消息保留在 Dapr 运行时中。 .NET SDK 支持一个高性能队列,用于在处理挂起时在您的应用程序中维护这些消息的本地缓存。这些消息将保留在队列中,直到每个消息的处理超时或采取响应操作(通常在处理成功或失败后)。在 Dapr 运行时收到此响应操作之前,消息将由 Dapr 保留,并在服务故障时可用。
可用的各种响应操作如下:
响应操作 描述 重试 事件应在将来再次传递。 丢弃 事件应被删除(或转发到死信队列,如果已配置)并且不再尝试。 成功 事件应被删除,因为它已成功处理。
处理程序将一次只接收一条消息,如果为订阅提供了取消令牌,则将在处理程序调用期间提供此令牌。
处理程序必须配置为返回一个 Task<TopicResponseAction>
,指示这些操作之一,即使是从 try/catch 块中返回。如果您的处理程序未捕获异常,订阅将在订阅注册期间配置的选项中使用响应操作。
以下演示了示例中提供的示例消息处理程序:
Task < TopicResponseAction > HandleMessageAsync ( TopicMessage message , CancellationToken cancellationToken = default )
{
try
{
//对消息做一些事情
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
组成。
其他选项如下:
属性名称 描述 Metadata 额外的订阅元数据 DeadLetterTopic 发送丢弃消息的死信主题的可选名称。 MaximumQueuedMessages 默认情况下,内部队列没有强制的最大边界,但设置此属性将施加上限。 MaximumCleanupTimeout 当订阅被处理或令牌标记取消请求时,这指定了处理内部队列中剩余消息的最大时间。
然后按以下示例配置订阅:
var messagingClient = app . Services . GetRequiredService < DaprPublishSubscribeClient >();
var cancellationTokenSource = new CancellationTokenSource ( TimeSpan . FromSeconds ( 60 )); //覆盖默认的30秒
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 秒。
6.2 - DaprPublishSubscribeClient 使用指南 使用 DaprPublishSubscribeClient 的基本提示和建议
生命周期管理 DaprPublishSubscribeClient
是 Dapr 客户端的一个版本,专门用于与 Dapr 消息 API 交互。它可以与 DaprClient
和其他 Dapr 客户端一起注册而不会出现问题。
它通过 TCP 套接字与 Dapr sidecar 通信,维护对网络资源的访问,并实现了 IAsyncDisposable
接口以支持资源的快速清理。
为了获得最佳性能,建议创建一个长生命周期的 DaprPublishSubscribeClient
实例,并在整个应用程序中共享使用。DaprPublishSubscribeClient
实例是线程安全的,适合共享使用。
可以通过依赖注入来实现这一点。注册方法支持以单例、作用域实例或瞬态(每次注入时重新创建)的方式进行注册,但也可以通过 IConfiguration
或其他注入服务的值来注册,这在每个类中从头创建客户端时是不切实际的。
避免为每个操作创建一个 DaprPublishSubscribeClient
并在操作完成后销毁它。DaprPublishSubscribeClient
应仅在您不再希望接收订阅事件时才被销毁,因为销毁它将取消正在进行的新事件接收。
通过 DaprPublishSubscribeClientBuilder 配置 DaprPublishSubscribeClient 可以通过在 DaprPublishSubscribeClientBuilder
类上调用方法来配置 DaprPublishSubscribeClient
,然后调用 .Build()
来创建客户端本身。每个 DaprPublishSubscribeClient
的设置是独立的,并且在调用 .Build()
之后无法更改。
var daprPubsubClient = new DaprPublishSubscribeClientBuilder ()
. UseDaprApiToken ( "abc123" ) // 指定用于认证到其他 Dapr sidecar 的 API 令牌
. Build ();
DaprPublishSubscribeClientBuilder
包含以下设置:
Dapr sidecar 的 HTTP 端点 Dapr sidecar 的 gRPC 端点 用于配置 JSON 序列化的 JsonSerializerOptions
对象 用于配置 gRPC 的 GrpcChannelOptions
对象 用于认证请求到 sidecar 的 API 令牌 用于创建 SDK 使用的 HttpClient
实例的工厂方法 用于在向 sidecar 发出请求时使用的 HttpClient
实例的超时 SDK 将读取以下环境变量来配置默认值:
DAPR_HTTP_ENDPOINT
:用于查找 Dapr sidecar 的 HTTP 端点,例如:https://dapr-api.mycompany.com
DAPR_GRPC_ENDPOINT
:用于查找 Dapr sidecar 的 gRPC 端点,例如:https://dapr-grpc-api.mycompany.com
DAPR_HTTP_PORT
:如果未设置 DAPR_HTTP_ENDPOINT
,则用于查找 Dapr sidecar 的本地 HTTP 端点DAPR_GRPC_PORT
:如果未设置 DAPR_GRPC_ENDPOINT
,则用于查找 Dapr sidecar 的本地 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 ();
7 - Dapr .NET SDK 的错误处理 探索如何在 Dapr .NET SDK 中进行错误处理。
7.1 - Dapr .NET SDK 中更全面的错误模型 了解如何在 .NET SDK 中使用更全面的错误模型。
Dapr .NET SDK 支持由 Dapr 运行时实现的更全面的错误模型。这个模型为应用程序提供了一种丰富错误信息的方式,提供更多上下文信息,使应用程序的用户能够更好地理解问题并更快地解决。您可以在这里 阅读更多关于更全面错误模型的信息,并可以在这里 找到实现这些错误的 Dapr proto 文件。
Dapr .NET SDK 实现了 Dapr 运行时支持的所有细节,这些细节在 Dapr.Common.Exceptions
命名空间中实现,并可以通过 DaprException
的扩展方法 TryGetExtendedErrorInfo
进行访问。目前,此细节提取仅支持存在细节的 RpcException
。
// 扩展错误信息的示例用法
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 告知客户端在重试之前应等待多长时间的信息。提供一个 DaprRetryDelay
,其属性包括 Second
(秒偏移)和 Nano
(纳秒偏移)。
DebugInfo 服务器提供的调试信息。包含 StackEntries
(包含堆栈跟踪的字符串集合)和 Detail
(进一步的调试信息)。
QuotaFailure 与可能已达到的某些配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations
,是 DaprQuotaFailureViolation
的集合,每个都包含 Subject
(请求的主题)和 Description
(有关失败的更多信息)。
PreconditionFailure 告知客户端某些必需的前置条件未满足的信息。具有一个属性 Violations
,是 DaprPreconditionFailureViolation
的集合,每个都有 Subject
(前置条件失败发生的主题,例如 “Azure”)、Type
(前置条件类型的表示,例如 “TermsOfService”)和 Description
(进一步描述,例如 “ToS 必须被接受。")。
RequestInfo 服务器返回的信息,可用于服务器识别客户端请求。包含 RequestId
和 ServingData
属性,RequestId
是服务器可以解释的某个字符串(例如 UID),ServingData
是构成请求一部分的任意数据。
LocalizedMessage 包含本地化消息及其语言环境。包含 Locale
(语言环境,例如 “en-US”)和 Message
(本地化消息)。
BadRequest 描述错误请求字段。包含 DaprBadRequestDetailFieldViolation
的集合,每个都有 Field
(请求中有问题的字段,例如 ‘first_name’)和 Description
(详细说明原因,例如 “first_name 不能包含特殊字符”)。
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
(与错误相关的资源的进一步信息,例如 “缺少使用此资源的权限”)。
Unknown 当详细类型 URL 无法映射到正确的 DaprExtendedErrorDetail
实现时返回。提供一个属性 TypeUrl
(无法解析的类型 URL,例如 “type.googleapis.com/Google.rpc.UnrecognizedType”)。
8 - 使用 Dapr .NET SDK 开发应用程序 了解 .NET Dapr 应用程序的本地开发集成选项
同时管理多个任务 通常情况下,使用您喜欢的 IDE 或编辑器启动应用程序时,您只需运行一个任务:您正在调试的应用程序。然而,开发微服务要求您在本地开发过程中同时管理多个任务 。一个微服务应用程序包含多个服务,您可能需要同时运行这些服务,并管理依赖项(如状态存储)。
将 Dapr 集成到您的开发过程中意味着您需要管理以下事项:
您想要运行的每个服务 每个服务的 Dapr sidecar Dapr 组件和配置清单 额外的依赖项,如状态存储 可选:用于 actor 的 Dapr placement 服务 本文档假设您正在构建一个生产应用程序,并希望创建一套可重复且稳健的开发实践。这里的指导是通用的,适用于任何使用 Dapr 的 .NET 服务器应用程序(包括 actor)。
组件管理 您有两种主要方法来存储 Dapr 本地开发的组件定义:
使用默认位置 (~/.dapr/components
) 使用您自定义的位置 在您的源代码库中创建一个文件夹来存储组件和配置,这样可以方便地对这些定义进行版本控制和共享。本文假设您在应用程序源代码旁边创建了一个文件夹来存储这些文件。
开发选项 选择以下链接之一以了解您可以在本地开发场景中使用的工具。这些文章按投入程度从低到高排序。您可能希望阅读所有文章以全面了解可用选项。
8.1 - 使用 Dapr CLI 进行 Dapr .NET SDK 开发 了解如何使用 Dapr CLI 进行本地开发
Dapr CLI 可以将其视为 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南 的补充 。
Dapr CLI 通过初始化本地的 Redis 容器、Zipkin 容器、placement 服务和 Redis 的组件清单,为您提供了一个良好的基础环境。这使您能够在全新安装且无需额外设置的情况下使用以下功能模块:
您可以使用 dapr run
命令来运行 .NET 服务,作为本地开发的一种策略。为每个服务运行此命令以启动您的应用程序。
优势: 由于这是 Dapr 默认安装的一部分,因此设置简单劣势: 这会在您的机器上运行长时间的 Docker 容器,可能不太理想劣势: 这种方法的可扩展性较差,因为需要为每个服务运行一个单独的命令使用 Dapr CLI 对于每个服务,您需要选择:
用于寻址的唯一应用 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
参数来修改上述命令。
下一步 如果您需要调试,请使用调试器的附加功能附加到其中一个正在运行的进程。
如果您想扩展这种方法,请考虑编写一个脚本来为您的整个应用程序自动化此过程。
8.2 - 使用 .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 的文档 这里 。
先决条件 通过 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 应用程序。从同一目录中,使用以下命令创建一个名为 MyApp
的空 ASP.NET Core 项目。它将在 MyApp\MyApp.csproj
中相对于您的当前目录创建。
接下来,我们将配置 AppHost 项目以添加支持本地 Dapr 开发所需的包。使用以下命令导航到 AppHost 目录,并从 NuGet 安装 Aspire.Hosting.Dapr
包到项目中。我们还将添加对 MyApp
项目的引用,以便在注册过程中引用它。
cd aspiredemo.AppHost
dotnet add package Aspire.Hosting.Dapr
dotnet add reference ../MyApp/
接下来,我们需要将 Dapr 配置为与您的项目一起加载的资源。在您喜欢的 IDE 中打开该项目中的 Program.cs
文件。它应类似于以下内容:
var builder = DistributedApplication . CreateBuilder ( args );
builder . Build (). Run ();
如果您熟悉 ASP.NET Core 项目中使用的依赖注入方法或其他使用 Microsoft.Extensions.DependencyInjection
功能的项目,您会发现这将是一个熟悉的体验。
因为我们已经添加了对 MyApp
的项目引用,我们需要在此配置中添加一个引用。在 builder.Build().Run()
行之前添加以下内容:
var myApp = builder
. AddProject < Projects . MyApp >( "myapp" )
. WithDaprSidecar ();
因为项目引用已添加到此解决方案中,您的项目在此处显示为 Projects.
命名空间中的一个类型。您为项目分配的变量名称在本教程中并不重要,但如果您想在此项目和另一个项目之间创建引用以使用 Aspire 的服务发现功能,则会使用它。
添加 .WithDaprSidecar()
将 Dapr 配置为 .NET Aspire 资源,以便在项目运行时,sidecar 将与您的应用程序一起部署。这接受许多不同的选项,并可以选择性地配置,如以下示例所示:
DaprSidecarOptions sidecarOptions = new ()
{
AppId = "my-other-app" ,
AppPort = 8080 , //注意,如果您打算配置 pubsub、actor 或 workflow,从 Aspire v9.0 开始,此参数是必需的
DaprGrpcPort = 50001 ,
DaprHttpPort = 3500 ,
MetricsPort = 9090
};
builder
. AddProject < Projects . MyOtherApp >( "myotherapp" )
. WithReference ( myApp )
. WithDaprSidecar ( sidecarOptions );
如上例所示,从 .NET Aspire 9.0 开始,如果您打算使用 Dapr 需要调用到您的应用程序的任何功能,例如 pubsub、actor 或 workflow,您将需要指定您的 AppPort 作为配置选项,因为 Aspire 不会在运行时自动将其传递给 Dapr。预计这种行为将在未来的版本中更改,因为修复已合并并可以在
这里 跟踪。
当您在 IDE 中打开解决方案时,确保 aspiredemo.AppHost
被配置为您的启动项目,但当您在调试配置中启动它时,您会注意到您的集成控制台应反映您预期的 Dapr 日志,并且它将可用于您的应用程序。
8.3 - 使用 Project Tye 进行 Dapr .NET SDK 开发 了解如何使用 Project Tye 进行本地开发
Project Tye .NET Project Tye 是一个专为简化运行多个 .NET 服务而设计的微服务开发工具。Tye 允许您将多个 .NET 服务、进程和容器镜像的配置整合为一个可运行的应用程序。
对于 .NET Dapr 开发者来说,Tye 的优势在于:
Tye 可以自动化使用 dapr CLI Tye 遵循 .NET 的约定,对 .NET 服务几乎无需额外配置 Tye 能够管理容器中依赖项的生命周期 优缺点:
优点: Tye 可以自动化上述所有步骤。您无需再担心端口或应用程序 ID 等细节。优点: 由于 Tye 也可以管理容器,您可以将这些容器作为应用程序的一部分定义,并避免机器上长时间运行的容器。使用 Tye 按照 Tye 入门指南 安装 tye
CLI,并为您的应用程序创建 tye.yaml
文件。
接下来,按照 Tye Dapr 配方 中的步骤添加 Dapr。确保在 tye.yaml
中使用 components-path
指定组件文件夹的相对路径。
然后,添加任何额外的容器依赖项,并将组件定义添加到您之前创建的文件夹中。
您应该得到如下内容:
name : store-application
extensions :
# Dapr 的配置在这里。
- name : dapr
components-path : <components-path>
# 要运行的服务在这里。
services :
# 名称将用作应用程序 ID。对于 .NET 项目,Tye 只需要项目文件的路径。
- name : orders
project : orders/orders.csproj
- name : products
project : products/products.csproj
- name : store
project : store/store.csproj
# 您想要运行的容器需要一个镜像名称和一组要暴露的端口。
- name : redis
image : redis
bindings :
- port : 6973
将 tye.yaml
和应用程序代码一起提交到源代码管理中。
您现在可以使用 tye run
从一个终端启动整个应用程序。运行时,Tye 在 http://localhost:8000
提供一个仪表板以查看应用程序状态和日志。
下一步 Tye 会将您的服务作为标准 .NET 进程在本地运行。如果您需要调试,可以使用调试器附加到正在运行的进程之一。由于 Tye 了解 .NET,它可以在启动时暂停进程以便进行调试。
如果您希望在容器中进行本地测试,Tye 还提供了一个选项,可以在容器中运行您的服务。
8.4 - 使用 Docker-Compose 进行 Dapr .NET SDK 开发 了解如何使用 Docker-Compose 进行本地开发
Docker-Compose 这可以看作是 .NET 伴侣指南:使用 Docker 的 Dapr 自托管指南 的补充。
docker-compose
是 Docker Desktop 附带的一个命令行工具,您可以用它同时运行多个容器。它提供了一种自动化管理多个容器生命周期的方法,为面向 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 文件至少应包含以下内容:
容器之间通信所需的网络 每个服务的容器 一个 <service>-daprd
sidecar 容器,指定服务的端口和 app-id 在容器中运行的其他依赖项(例如 redis) 可选:Dapr placement 容器(用于 actor) 您还可以查看 eShopOnContainers 示例应用程序中的更大示例。
9 - Dapr .NET SDK 故障排除与调试 掌握使用 Dapr .NET SDK 进行故障排除与调试的实用方法和指南
9.1 - 使用 .NET SDK 进行 Pub/Sub 故障排查 使用 .NET SDK 进行 Pub/Sub 故障排查
Pub/Sub 故障排查 Pub/Sub 的常见问题是应用程序中的 Pub/Sub 端点未被调用。
这个问题可以分为几个层次,每个层次有不同的解决方案:
应用程序没有接收到来自 Dapr 的任何流量 应用程序没有向 Dapr 注册 Pub/Sub 端点 Pub/Sub 端点已在 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 会向您的应用程序发送一些请求以进行配置。如果找不到这些请求,则意味着出现了问题。请通过问题或 Discord 请求帮助(包括日志)。如果您看到对应用程序的请求,请继续执行下一步。
步骤 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 并包含一些 Pub/Sub 条目 如果您在此测试的 JSON 输出中有条目,则问题出在其他地方,请继续执行下一步。
选项 1:响应不是 200,或不包含 JSON 如果响应不是 200 或不包含 JSON,则 MapSubscribeHandler()
端点未被访问。
确保在 Startup.cs
中有如下代码并重复测试。
app . UseRouting ();
app . UseCloudEvents ();
app . UseEndpoints ( endpoints =>
{
endpoints . MapSubscribeHandler (); // 这是 Dapr 订阅处理程序
endpoints . MapControllers ();
});
如果添加订阅处理程序没有解决问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
文件的内容。
选项 2:响应包含 JSON 但为空(如 []
) 如果 JSON 输出是一个空数组(如 []
),则订阅处理程序已注册,但没有注册主题端点。
如果您使用控制器进行 Pub/Sub,您应该有一个类似的方法:
[Topic("pubsub", "deposit")]
[HttpPost("deposit")]
public async Task < ActionResult > Deposit (...)
// 使用 Pub/Sub 路由
[Topic("pubsub", "transactions", "event.type == \"withdraw.v2\"", 1)]
[HttpPost("withdraw")]
public async Task < ActionResult > Withdraw (...)
在此示例中,Topic
和 HttpPost
属性是必需的,但其他细节可能不同。
如果您使用路由进行 Pub/Sub,您应该有一个类似的端点:
endpoints . MapPost ( "deposit" , ...). WithTopic ( "pubsub" , "deposit" );
在此示例中,调用 WithTopic(...)
是必需的,但其他细节可能不同。
在更正此代码并重新测试后,如果 JSON 输出仍然是空数组(如 []
),请在此仓库中打开一个问题,并包括 Startup.cs
和您的 Pub/Sub 端点的内容。
步骤 4:验证端点可达性 在此步骤中,我们将验证注册的 Pub/Sub 条目是否可达。上一步应该让您得到如下的 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 测试工具)访问注册的 Pub/Sub 端点之一。
假设您的应用程序监听端口为 5000,并且您的一个 Pub/Sub 路由是 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 直接发送 Pub/Sub 消息并比较日志输出。
示例命令:
dapr publish --pubsub pubsub --topic withdraw --data '{}'
如果在这样做之后您仍然不理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。
选项 1:路由未执行 如果您在日志中没有看到 Microsoft.AspNetCore.Routing.EndpointMiddleware
的条目,则意味着请求被其他东西处理了。通常情况下,问题是一个行为不当的中间件。请求的其他日志可能会给您一个线索。
如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。
选项 2:路由选择了错误的端点 如果您在日志中看到 Microsoft.AspNetCore.Routing.EndpointMiddleware
的条目,但它包含错误的端点,则意味着您有一个路由冲突。被选择的端点将出现在日志中,这应该能给您一个关于冲突原因的想法。
如果您需要帮助理解问题,请在此仓库中打开一个问题,并包括您的 Startup.cs
的内容。