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

Return to the regular view of this page.

Dapr 软件开发工具包 (SDKs)

使用您喜欢的语言与 Dapr 一起工作

Dapr SDKs 是将 Dapr 集成到应用程序中的最简单方法。选择您喜欢的语言,几分钟内即可开始使用 Dapr。

SDK 包

选择您偏好的语言以了解有关客户端、服务扩展、actor 和工作流包的更多信息。

  • 客户端: Dapr 客户端允许您调用 Dapr 构建块 API 并执行每个构建块的操作。
  • 服务扩展: Dapr 服务扩展使您能够创建可被其他服务调用的服务并订阅主题。
  • actor: Dapr actor SDK 允许您构建具有方法、状态、计时器和持久性提醒的虚拟 actor。
  • 工作流: Dapr 工作流使您能够可靠地编写长时间运行的业务逻辑和集成。

SDK 语言

语言状态客户端服务扩展actor工作流
.NET稳定ASP.NET Core
Python稳定gRPC
FastAPI
Flask
Java稳定Spring Boot
Quarkus
Go稳定
PHP稳定
JavaScript稳定
C++开发中
Rust开发中

进一步阅读

1 - Dapr .NET SDK

用于开发 Dapr 应用程序的 .NET SDK 包

Dapr 提供多种包以协助 .NET 应用程序的开发。通过这些包,您可以使用 Dapr 创建 .NET 客户端、服务器和虚拟 actor。

先决条件

安装

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

dotnet add package Dapr.Client

体验

尝试 Dapr .NET SDK。通过 .NET 快速入门和教程来探索 Dapr 的实际应用:

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

可用包

客户端

创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 .NET 客户端。

服务器

使用 Dapr SDK 编写 .NET 服务器和服务。包括对 ASP.NET 的支持。

Actors

在 .NET 中创建具有状态、提醒/计时器和方法的虚拟 actor。

工作流

创建和管理与其他 Dapr API 一起工作的工作流。

作业

创建和管理 .NET 中作业的调度和编排。

AI

在 .NET 中创建和管理 AI 操作

更多信息

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

开发

了解 .NET Dapr 应用程序的本地开发选项

NuGet 包

用于将 .NET SDK 添加到您的 .NET 应用程序的 Dapr 包。


1.1 - 开始使用 Dapr 客户端 .NET SDK

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

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

构建块

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

调用服务

HTTP

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

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

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

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

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

gRPC

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

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

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

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

保存和获取应用程序状态

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

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

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

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

查询状态 (Alpha)

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

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

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

发布消息

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

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

与输出绑定交互

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

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

检索秘密

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

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

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

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

获取配置键

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

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

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

订阅配置键

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

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

分布式锁 (Alpha)

获取锁

using System;
using Dapr.Client;

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

解锁现有锁

using System;
using Dapr.Client;

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

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

管理工作流实例 (Alpha)

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

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

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

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

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

Sidecar APIs

Sidecar 健康

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

轮询健康状态

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

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

var isDaprReady = await client.CheckHealthAsync();

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

轮询健康状态(出站)

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

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

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

var isDaprComponentsReady = await client.CheckOutboundHealthAsync();

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

等待 sidecar

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

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

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

关闭 sidecar

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

相关链接

1.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));
});

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

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

手动实例化

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

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

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

配置 DaprClient

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

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

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

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

DaprClientBuilder 上配置

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

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

IConfiguration 配置

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

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

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

从环境变量配置

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

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

配置 gRPC 通道选项

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

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

使用 DaprClient 进行取消

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

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

理解 DaprClient 的 JSON 序列化

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

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

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

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

JSON 序列化的简单指导

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

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

多态性和序列化

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

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

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

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

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

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

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

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

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

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

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

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

错误处理

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

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

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

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

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

1.2 - Dapr actors .NET SDK

快速掌握使用 Dapr actors .NET SDK 的方法

借助 Dapr actor 包,您可以在 .NET 应用程序中轻松与 Dapr 的虚拟 actor 进行交互。

要开始,请参阅 Dapr actors 指南。

1.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

确定 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

您可以使用 UUID 匹配调用者和被调用者一侧的异常。以下是异常详细信息的示例:

Dapr.Actors.ActorMethodInvocationException: 远程 actor 方法异常,详细信息:异常:NotImplementedException,方法名称:ExceptionExample,行号:14,异常 uuid:d291a006-84d5-42c4-b39e-d6300e9ac38b

下一步

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

1.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可以实现IDisposableIAsyncDisposable。建议您依赖依赖注入进行资源管理,而不是在应用程序代码中实现释放功能。仅在确实必要的情况下提供释放支持。

日志记录

在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.csConfigureServices的一部分。您可以通过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();
    });
}

UseRoutingUseEndpoints调用是配置路由所必需的。通过在端点中间件中添加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示例

1.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))],这告诉数据契约序列化器 DerivedClassBaseClass 的可能实现,它可能需要序列化或反序列化。如果没有此属性,当序列化器遇到一个实际上是 DerivedClass 类型的 BaseClass 实例时,它将不知道如何处理派生类型,这可能导致序列化异常。通过将所有可能的派生类型指定为已知类型,您可以确保序列化器能够正确处理类型及其成员。

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

1.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 的客户端

由于客户端项目可能依赖于它,最好将其定义在与 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 的方法。

准备工作

步骤 0:准备

我们将创建 3 个项目,请选择一个空目录开始,并在您选择的终端中打开它。

步骤 1:创建 actor 接口

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

actor 接口定义如下要求:

  • actor 接口必须继承 Dapr.Actors.IActor 接口
  • actor 方法的返回类型必须是 TaskTask<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}");
        }
    }
}

运行代码

您创建的项目现在可以测试示例。

  1. 运行 MyActorService

    由于 MyActorService 托管 actor,因此需要使用 Dapr CLI 运行。

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

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

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

    MyActorClient 作为客户端,可以通过 dotnet run 正常运行。

    打开一个新终端并导航到 MyActorClient 目录。然后运行项目:

    dotnet run
    

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

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

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

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

相关链接

1.3 - Dapr Workflow .NET SDK

快速上手并掌握 Dapr Workflow 和 Dapr .NET SDK 的使用

1.3.1 - DaprWorkflowClient 使用

使用 DaprWorkflowClient 的基本提示和建议

生命周期管理

DaprWorkflowClient 可以访问网络资源,这些资源通过 TCP 套接字与 Dapr sidecar 以及其他用于管理和操作工作流的类型进行通信。DaprWorkflowClient 实现了 IAsyncDisposable 接口,以便快速清理资源。

依赖注入

AddDaprWorkflow() 方法用于通过 ASP.NET Core 的依赖注入机制注册 Dapr 工作流服务。此方法需要一个选项委托,用于定义您希望在应用程序中注册和使用的每个工作流和活动。

单例注册

默认情况下,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 添加日志记录或通过注入 DaprClientDaprJobsClient 访问其他 Dapr 组件变得简单。

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

在工作流中使用 ILogger

由于工作流必须是确定性的,因此不能将任意服务注入其中。例如,如果您能够将标准 ILogger 注入到工作流中,并且由于错误需要重放它,日志记录的重复操作可能会导致混淆,因为这些操作实际上并没有再次发生。为了解决这个问题,工作流中提供了一种重放安全的日志记录器。它只会在工作流第一次运行时记录事件,而在重放时不会记录任何内容。

这种日志记录器可以通过工作流实例上的 WorkflowContext 中的方法获取,并可以像使用 ILogger 实例一样使用。

一个展示此功能的完整示例可以在 .NET SDK 仓库 中找到,以下是该示例的简要摘录。

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

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

1.3.2 - 如何:在 .NET SDK 中编写和管理 Dapr 工作流

学习如何使用 .NET SDK 编写和管理 Dapr 工作流

我们来创建一个 Dapr 工作流并通过控制台调用它。在提供的订单处理工作流示例中,控制台会提示如何进行购买和补货。在本指南中,您将:

  • 部署一个 .NET 控制台应用程序 (WorkflowConsoleApp)。
  • 使用 .NET 工作流 SDK 和 API 调用来启动和查询工作流实例。

在 .NET 示例项目里:

先决条件

设置环境

克隆 .NET SDK 仓库

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

从 .NET SDK 根目录,导航到 Dapr 工作流示例。

cd examples/Workflow

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 WorkflowConsoleApp 目录。

cd WorkflowConsoleApp

启动程序。

dotnet run

在一个新的终端中,再次导航到 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

启动工作流

要启动工作流,您有两种选择:

  1. 按照控制台提示的指示。
  2. 使用工作流 API 并直接向 Dapr 发送请求。

本指南重点介绍工作流 API 选项。

运行以下命令以启动工作流。

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 工作流

下一步

1.4 - Dapr AI .NET SDK

快速上手使用 Dapr AI .NET SDK

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

目前,Dapr 提供了一个会话 API,用于与大型语言模型进行交互。要开始使用此功能,请参阅 Dapr 会话 AI 指南。

1.4.1 - Dapr AI 客户端

学习如何创建 Dapr AI 客户端

Dapr AI 客户端包使您能够与 Dapr 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();

1.4.2 - 如何在 .NET SDK 中创建和使用 Dapr AI 会话

学习如何使用 .NET SDK 创建和使用 Dapr 会话 AI 客户端

前提条件

安装

要开始使用 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 接口,以便从大型语言模型发送和接收消息。

发送消息

1.5 - Dapr Jobs .NET SDK

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

使用 Dapr Job 包,您可以在 .NET 应用程序中与 Dapr Job API 进行交互。通过预设的计划,您可以安排未来的操作,并可选择附带数据。

要开始使用,请查看 Dapr Jobs 指南,并参考 最佳实践文档 以获取更多指导。

1.5.1 - 如何:在 .NET SDK 中编写和管理 Dapr 任务

学习如何使用 .NET SDK 编写和管理 Dapr 任务

我们来创建一个端点,该端点将在 Dapr 任务触发时被调用,然后在同一个应用中调度该任务。我们将使用此处提供的简单示例,进行以下演示,并通过它来解释如何使用间隔或 Cron 表达式自行调度一次性或重复性任务。在本指南中,您将:

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

在 .NET 示例项目中:

  • 主要的 Program.cs 文件是整个演示的核心。

前提条件

设置环境

克隆 .NET SDK 仓库

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

从 .NET SDK 根目录,导航到 Dapr 任务示例。

cd examples/Jobs

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 .NET 程序和一个 Dapr sidecar。导航到 JobsSample 目录。

cd 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 所示。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprJobsClient() 注册将自动使用这些值覆盖其各自的默认值。

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

通过 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 令牌。

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

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

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

通过前缀环境变量配置

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

KeyValue
myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。

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 方法都支持取消令牌,并在未设置时使用默认令牌。

有三种不同的方式来设置任务,具体取决于您想要如何配置调度:

一次性任务

一次性任务就是这样;它将在某个时间点运行,并且不会重复。这种方法要求您选择一个任务名称并指定一个触发时间。

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
scheduledTimeDateTime任务应运行的时间点。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 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 构建块中工作的提醒。这些任务也可以通过许多可选参数进行调度:

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
intervalTimeSpan任务应触发的间隔。
startingFromDateTime任务调度应开始的时间点。
repeatsint任务应触发的最大次数。
ttl任务何时过期且不再触发。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 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 表达式调度的。这提供了更多基于日历的控制,以便在任务触发时使用日历值在表达式中。与其他选项一样,这些任务也可以通过许多可选参数进行调度:

参数名称类型描述必需
jobNamestring正在调度的任务的名称。
cronExpressionstring指示任务应触发的 systemd 类似 Cron 表达式。
startingFromDateTime任务调度应开始的时间点。
repeatsint任务应触发的最大次数。
ttl任务何时过期且不再触发。
payloadReadOnlyMemory触发时提供给调用端点的任务数据。
cancellationTokenCancellationToken用于提前取消操作,例如由于操作超时。

可以从 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);
    }
}

1.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。在非法参数值的情况下,将抛出适当的标准异常(例如 ArgumentOutOfRangeExceptionArgumentNullException),并附上有问题的参数名称。对于其他任何情况,将抛出 DaprException

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

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

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

1.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 指南,并参考最佳实践文档以获取更多指导。

1.6.1 - 如何:在 .NET SDK 中编写和管理 Dapr 流式订阅

学习如何使用 .NET SDK 编写和管理 Dapr 流式订阅

我们来创建一个使用流式功能的发布/订阅主题或队列的订阅。我们将使用此处提供的简单示例,进行演示,并逐步讲解如何在运行时配置消息处理程序,而无需预先配置端点。在本指南中,您将会学习如何:

前提条件

设置环境

克隆 .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 所示。相反,通过填充通过依赖注入提供的 IConfigurationAddDaprPubSubClient() 注册将自动使用这些值覆盖其各自的默认值。

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

通过 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_ENDPOINThttp://localhost:54321
DAPR_API_TOKENabc123
var builder = WebApplication.CreateBuilder();

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

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

通过前缀环境变量配置

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

myapp_DAPR_HTTP_ENDPOINThttp://localhost:54321
myapp_DAPR_API_TOKENabc123

这些环境变量将在以下示例中加载到注册的配置中,并在没有附加前缀的情况下提供。

var builder = WebApplication.CreateBuilder();

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

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

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

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


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

        //使用 `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 秒。

1.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();

1.7 - Dapr .NET SDK 的错误处理

探索如何在 Dapr .NET SDK 中进行错误处理。

1.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

  1. RetryInfo

  2. DebugInfo

  3. QuotaFailure

  4. PreconditionFailure

  5. RequestInfo

  6. LocalizedMessage

  7. BadRequest

  8. ErrorInfo

  9. Help

  10. ResourceInfo

  11. Unknown

RetryInfo

告知客户端在重试之前应等待多长时间的信息。提供一个 DaprRetryDelay,其属性包括 Second(秒偏移)和 Nano(纳秒偏移)。

DebugInfo

服务器提供的调试信息。包含 StackEntries(包含堆栈跟踪的字符串集合)和 Detail(进一步的调试信息)。

QuotaFailure

与可能已达到的某些配额相关的信息,例如 API 的每日使用限制。它有一个属性 Violations,是 DaprQuotaFailureViolation 的集合,每个都包含 Subject(请求的主题)和 Description(有关失败的更多信息)。

PreconditionFailure

告知客户端某些必需的前置条件未满足的信息。具有一个属性 Violations,是 DaprPreconditionFailureViolation 的集合,每个都有 Subject(前置条件失败发生的主题,例如 “Azure”)、Type(前置条件类型的表示,例如 “TermsOfService”)和 Description(进一步描述,例如 “ToS 必须被接受。")。

RequestInfo

服务器返回的信息,可用于服务器识别客户端请求。包含 RequestIdServingData 属性,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”)。

1.8 - 使用 Dapr .NET SDK 开发应用程序

了解 .NET Dapr 应用程序的本地开发集成选项

同时管理多个任务

通常情况下,使用您喜欢的 IDE 或编辑器启动应用程序时,您只需运行一个任务:您正在调试的应用程序。然而,开发微服务要求您在本地开发过程中同时管理多个任务。一个微服务应用程序包含多个服务,您可能需要同时运行这些服务,并管理依赖项(如状态存储)。

将 Dapr 集成到您的开发过程中意味着您需要管理以下事项:

  • 您想要运行的每个服务
  • 每个服务的 Dapr sidecar
  • Dapr 组件和配置清单
  • 额外的依赖项,如状态存储
  • 可选:用于 actor 的 Dapr placement 服务

本文档假设您正在构建一个生产应用程序,并希望创建一套可重复且稳健的开发实践。这里的指导是通用的,适用于任何使用 Dapr 的 .NET 服务器应用程序(包括 actor)。

组件管理

您有两种主要方法来存储 Dapr 本地开发的组件定义:

  • 使用默认位置 (~/.dapr/components)
  • 使用您自定义的位置

在您的源代码库中创建一个文件夹来存储组件和配置,这样可以方便地对这些定义进行版本控制和共享。本文假设您在应用程序源代码旁边创建了一个文件夹来存储这些文件。

开发选项

选择以下链接之一以了解您可以在本地开发场景中使用的工具。这些文章按投入程度从低到高排序。您可能希望阅读所有文章以全面了解可用选项。

1.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 命令。

如果您的任何服务不接受 HTTP 流量,请通过删除 --app-port--urls 参数来修改上述命令。

下一步

如果您需要调试,请使用调试器的附加功能附加到其中一个正在运行的进程。

如果您想扩展这种方法,请考虑编写一个脚本来为您的整个应用程序自动化此过程。

1.8.2 - 使用 .NET Aspire 进行 Dapr .NET SDK 开发

了解如何使用 .NET Aspire 进行本地开发

.NET Aspire

.NET Aspire 是一款开发工具,旨在通过提供一个框架,简化外部软件与 .NET 应用程序的集成过程。该框架允许第三方服务轻松地与您的软件集成、监控和配置。

Aspire 通过与流行的 IDE(包括 Microsoft Visual StudioVisual Studio CodeJetBrains Rider 等)深度集成,简化了本地开发。在启动调试器的同时,自动启动并配置对其他集成(包括 Dapr)的访问。

虽然 Aspire 也支持将应用程序部署到各种云平台(如 Microsoft Azure 和 Amazon AWS),但本指南不涉及部署相关内容。更多信息请参阅 Aspire 的文档 这里

先决条件

通过 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 中相对于您的当前目录创建。

dotnet new web MyApp

接下来,我们将配置 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);

当您在 IDE 中打开解决方案时,确保 aspiredemo.AppHost 被配置为您的启动项目,但当您在调试配置中启动它时,您会注意到您的集成控制台应反映您预期的 Dapr 日志,并且它将可用于您的应用程序。

1.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 还提供了一个选项,可以在容器中运行您的服务。

1.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 负责运行容器,一旦您的服务在容器中,配置它就和其他编程技术类似。

总结这种方法:

  • 为每个服务创建一个 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 示例应用程序中的更大示例。

1.9 - Dapr .NET SDK 故障排除与调试

掌握使用 Dapr .NET SDK 进行故障排除与调试的实用方法和指南

1.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 的流量

  1. 像往常一样启动应用程序(dapr run ...)。确保在命令行中包含 --app-port 参数。Dapr 需要知道您的应用程序正在监听流量。默认情况下,ASP.NET Core 应用程序将在本地开发中监听 5000 端口的 HTTP。

  2. 等待 Dapr 启动完成

  3. 检查日志

您应该看到类似这样的日志条目:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/.....

在初始化过程中,Dapr 会向您的应用程序发送一些请求以进行配置。如果找不到这些请求,则意味着出现了问题。请通过问题或 Discord 请求帮助(包括日志)。如果您看到对应用程序的请求,请继续执行下一步。

步骤 3:验证端点注册

  1. 像往常一样启动应用程序(dapr run ...)。

  2. 使用命令行中的 curl(或其他 HTTP 测试工具)访问 /dapr/subscribe 端点。

假设您的应用程序监听端口为 5000,这里是一个示例命令:

curl http://localhost:5000/dapr/subscribe -v

对于配置正确的应用程序,输出应如下所示:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /dapr/subscribe HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 15 Jan 2021 22:31:40 GMT
< Content-Type: application/json
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
[{"topic":"deposit","route":"deposit","pubsubName":"pubsub"},{"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}]* Closing connection 0

特别注意 HTTP 状态码和 JSON 输出。

< HTTP/1.1 200 OK

200 状态码表示成功。

JSON 数据块是 /dapr/subscribe 的输出,由 Dapr 运行时处理。在这种情况下,它使用的是此仓库中的 ControllerSample - 这是正确输出的示例。

[
    {"topic":"deposit","route":"deposit","pubsubName":"pubsub"},
    {"topic":"withdraw","route":"withdraw","pubsubName":"pubsub"}
]

通过此命令的输出,您可以诊断问题或继续下一步。

选项 0:响应为 200 并包含一些 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(...)

在此示例中,TopicHttpPost 属性是必需的,但其他细节可能不同。


如果您使用路由进行 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 信息来测试应用程序。

  1. 像往常一样启动应用程序(dapr run ...)。

  2. 使用命令行中的 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 的内容。

2 - Dapr Go SDK

用于开发 Dapr 应用的 Go SDK 包

这是一个用于在 Go 中构建 Dapr 应用的客户端库。该客户端支持所有公共 Dapr API,专注于提供符合 Go 语言习惯的开发体验,提高开发者的工作效率。

客户端

使用 Go 客户端 SDK 来调用公共 Dapr API。 [**了解更多关于 Go 客户端 SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-client/)

服务

使用 Dapr 服务(回调)SDK 创建可被 Dapr 调用的服务。 [**了解更多关于 Go 服务(回调)SDK 的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/go/go-service/)

2.1 - Dapr 服务(回调)SDK for Go 入门指南

快速掌握如何使用 Dapr 服务(回调)SDK for Go

除了 Dapr API 客户端,Dapr Go SDK 还提供了用于启动 Dapr 回调服务的服务模块。您可以选择通过 gRPC 或 HTTP 来开发这些服务:

2.1.1 - 使用 Dapr HTTP 服务 SDK for Go 入门

如何使用 Dapr HTTP 服务 SDK for Go 快速上手

前置条件

首先导入 Dapr Go 的 service/http 包:

daprd "github.com/dapr/go-sdk/service/http"

创建和启动服务

要创建一个 HTTP Dapr 服务,首先需要在特定地址上创建一个 Dapr 回调实例:

s := daprd.NewService(":8080")

或者结合现有的 http.ServeMux 使用特定地址创建服务,以便与现有服务器集成:

mux := http.NewServeMux()
mux.HandleFunc("/", myOtherHandler)
s := daprd.NewServiceWithMux(":8080", mux)

创建服务实例后,你可以添加任意数量的事件、绑定和服务调用处理程序。定义好这些逻辑后,就可以启动服务:

if err := s.Start(); err != nil && err != http.ErrServerClosed {
	log.Fatalf("error: %v", err)
}

事件处理

要处理来自特定主题的事件,你需要在启动服务之前添加至少一个主题事件处理程序:

sub := &common.Subscription{
	PubsubName: "messages",
	Topic:      "topic1",
	Route:      "/events",
}
err := s.AddTopicEventHandler(sub, eventHandler)
if err != nil {
	log.Fatalf("error adding topic subscription: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
	// 处理事件
	return true, nil
}

你可以选择使用路由规则根据 CloudEvent 的内容将消息路由到不同的处理程序。

sub := &common.Subscription{
	PubsubName: "messages",
	Topic:      "topic1",
	Route:      "/important",
	Match:      `event.type == "important"`,
	Priority:   1,
}
err := s.AddTopicEventHandler(sub, importantHandler)
if err != nil {
	log.Fatalf("error adding topic subscription: %v", err)
}

你还可以创建一个自定义类型来实现 TopicEventSubscriber 接口以处理事件:

type EventHandler struct {
	// 事件处理程序所需的任何数据或引用。
}

func (h *EventHandler) Handle(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
    // 处理事件
    return true, nil
}

然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler

sub := &common.Subscription{
    PubsubName: "messages",
    Topic:      "topic1",
}
eventHandler := &EventHandler{
// 初始化字段
}
if err := s.AddTopicEventSubscriber(sub, eventHandler); err != nil {
    log.Fatalf("error adding topic subscription: %v", err)
}

服务调用处理程序

要处理服务调用,你需要在启动服务之前添加至少一个服务调用处理程序:

if err := s.AddServiceInvocationHandler("/echo", echoHandler); err != nil {
	log.Fatalf("error adding invocation handler: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
	log.Printf("echo - ContentType:%s, Verb:%s, QueryString:%s, %+v", in.ContentType, in.Verb, in.QueryString, string(in.Data))
	// 处理调用
	out = &common.Content{
		Data:        in.Data,
		ContentType: in.ContentType,
		DataTypeURL: in.DataTypeURL,
	}
	return
}

绑定调用处理程序

if err := s.AddBindingInvocationHandler("/run", runHandler); err != nil {
	log.Fatalf("error adding binding handler: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
	log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
	// 处理调用
	return nil, nil
}

相关链接

2.1.2 - 使用 Dapr 服务(回调)SDK for Go 入门

如何使用 Dapr 服务(回调)SDK for Go 快速上手

Dapr gRPC 服务 SDK for Go

前置条件

首先,导入 Dapr Go 服务/gRPC 包:

daprd "github.com/dapr/go-sdk/service/grpc"

创建和启动服务

要创建一个 gRPC Dapr 服务,首先需要在特定地址上创建一个 Dapr 回调实例:

s, err := daprd.NewService(":50001")
if err != nil {
    log.Fatalf("无法启动服务器: %v", err)
}

或者,使用地址和现有的 net.Listener,以便与现有的服务器监听器结合:

list, err := net.Listen("tcp", "localhost:0")
if err != nil {
	log.Fatalf("gRPC 监听器创建失败: %s", err)
}
s := daprd.NewServiceWithListener(list)

创建服务实例后,你可以为该服务添加任意数量的事件、绑定和服务调用处理程序,如下所示。定义好逻辑后,你就可以启动服务:

if err := s.Start(); err != nil {
    log.Fatalf("服务器错误: %v", err)
}

事件处理

要处理来自特定主题的事件,你需要在启动服务之前添加至少一个主题事件处理程序:

sub := &common.Subscription{
		PubsubName: "messages",
		Topic:      "topic1",
	}
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
    log.Fatalf("添加主题订阅时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
	log.Printf("事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
	// 在这里处理事件
	return true, nil
}

你可以选择使用路由规则根据 CloudEvent 的内容将消息路由到不同的处理程序。

sub := &common.Subscription{
	PubsubName: "messages",
	Topic:      "topic1",
	Route:      "/important",
	Match:      `event.type == "important"`,
	Priority:   1,
}
err := s.AddTopicEventHandler(sub, importantHandler)
if err != nil {
	log.Fatalf("添加主题订阅时出错: %v", err)
}

你还可以创建一个自定义类型来实现 TopicEventSubscriber 接口,以处理你的事件:

type EventHandler struct {
	// 你的事件处理程序需要的任何数据或引用。
}

func (h *EventHandler) Handle(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    log.Printf("事件 - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
    // 在这里处理事件
    return true, nil
}

然后可以使用 AddTopicEventSubscriber 方法添加 EventHandler

sub := &common.Subscription{
    PubsubName: "messages",
    Topic:      "topic1",
}
eventHandler := &EventHandler{
// 初始化任何字段
}
if err := s.AddTopicEventSubscriber(sub, eventHandler); err != nil {
    log.Fatalf("添加主题订阅时出错: %v", err)
}

服务调用处理程序

要处理服务调用,你需要在启动服务之前添加至少一个服务调用处理程序:

if err := s.AddServiceInvocationHandler("echo", echoHandler); err != nil {
    log.Fatalf("添加调用处理程序时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
	log.Printf("回声 - ContentType:%s, Verb:%s, QueryString:%s, %+v", in.ContentType, in.Verb, in.QueryString, string(in.Data))
	// 在这里处理调用
	out = &common.Content{
		Data:        in.Data,
		ContentType: in.ContentType,
		DataTypeURL: in.DataTypeURL,
	}
	return
}

绑定调用处理程序

要处理绑定调用,你需要在启动服务之前添加至少一个绑定调用处理程序:

if err := s.AddBindingInvocationHandler("run", runHandler); err != nil {
    log.Fatalf("添加绑定处理程序时出错: %v", err)
}

处理程序方法可以是任何符合预期签名的方法:

func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
	log.Printf("绑定 - Data:%v, Meta:%v", in.Data, in.Metadata)
	// 在这里处理调用
	return nil, nil
}

相关链接

2.2 - 使用 Dapr 客户端 Go SDK 入门

如何使用 Dapr Go SDK 快速上手

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

前提条件

在开始之前,您需要确保以下条件已满足:

导入客户端包

import "github.com/dapr/go-sdk/client"

错误处理

Dapr 的错误处理基于 gRPC 的丰富错误模型。以下代码示例展示了如何解析和处理错误详情:

if err != nil {
    st := status.Convert(err)

    fmt.Printf("Code: %s\n", st.Code().String())
    fmt.Printf("Message: %s\n", st.Message())

    for _, detail := range st.Details() {
        switch t := detail.(type) {
        case *errdetails.ErrorInfo:
            // 处理 ErrorInfo 详情
            fmt.Printf("ErrorInfo:\n- Domain: %s\n- Reason: %s\n- Metadata: %v\n", t.GetDomain(), t.GetReason(), t.GetMetadata())
        case *errdetails.BadRequest:
            // 处理 BadRequest 详情
            fmt.Println("BadRequest:")
            for _, violation := range t.GetFieldViolations() {
                fmt.Printf("- Key: %s\n", violation.GetField())
                fmt.Printf("- The %q field was wrong: %s\n", violation.GetField(), violation.GetDescription())
            }
        case *errdetails.ResourceInfo:
            // 处理 ResourceInfo 详情
            fmt.Printf("ResourceInfo:\n- Resource type: %s\n- Resource name: %s\n- Owner: %s\n- Description: %s\n",
                t.GetResourceType(), t.GetResourceName(), t.GetOwner(), t.GetDescription())
        case *errdetails.Help:
            // 处理 Help 详情
            fmt.Println("HelpInfo:")
            for _, link := range t.GetLinks() {
                fmt.Printf("- Url: %s\n", link.Url)
                fmt.Printf("- Description: %s\n", link.Description)
            }
        
        default:
            // 添加其他类型详情的处理
            fmt.Printf("Unhandled error detail type: %v\n", t)
        }
    }
}

构建块

Go SDK 允许您与所有 Dapr 构建块进行交互。

服务调用

要调用运行在 Dapr sidecar 中的另一个服务上的特定方法,Dapr 客户端 Go SDK 提供了两种选项:

调用不带数据的服务:

resp, err := client.InvokeMethod(ctx, "app-id", "method-name", "post")

调用带数据的服务:

content := &dapr.DataContent{
    ContentType: "application/json",
    Data:        []byte(`{ "id": "a123", "value": "demo", "valid": true }`),
}

resp, err = client.InvokeMethodWithContent(ctx, "app-id", "method-name", "post", content)

有关服务调用的完整指南,请访问 如何调用服务

状态管理

对于简单的用例,Dapr 客户端提供了易于使用的 SaveGetDelete 方法:

ctx := context.Background()
data := []byte("hello")
store := "my-store" // 在组件 YAML 中定义

// 使用键 key1 保存状态,默认选项:强一致性,最后写入
if err := client.SaveState(ctx, store, "key1", data, nil); err != nil {
    panic(err)
}

// 获取键 key1 的状态
item, err := client.GetState(ctx, store, "key1", nil)
if err != nil {
    panic(err)
}
fmt.Printf("data [key:%s etag:%s]: %s", item.Key, item.Etag, string(item.Value))

// 删除键 key1 的状态
if err := client.DeleteState(ctx, store, "key1", nil); err != nil {
    panic(err)
}

为了更细粒度的控制,Dapr Go 客户端公开了 SetStateItem 类型,可以用于更好地控制状态操作,并允许一次保存多个项目:

item1 := &dapr.SetStateItem{
    Key:  "key1",
    Etag: &ETag{
        Value: "1",
    },
    Metadata: map[string]string{
        "created-on": time.Now().UTC().String(),
    },
    Value: []byte("hello"),
    Options: &dapr.StateOptions{
        Concurrency: dapr.StateConcurrencyLastWrite,
        Consistency: dapr.StateConsistencyStrong,
    },
}

item2 := &dapr.SetStateItem{
    Key:  "key2",
    Metadata: map[string]string{
        "created-on": time.Now().UTC().String(),
    },
    Value: []byte("hello again"),
}

item3 := &dapr.SetStateItem{
    Key:  "key3",
    Etag: &dapr.ETag{
	Value: "1",
    },
    Value: []byte("hello again"),
}

if err := client.SaveBulkState(ctx, store, item1, item2, item3); err != nil {
    panic(err)
}

同样,GetBulkState 方法提供了一种在单个操作中检索多个状态项的方法:

keys := []string{"key1", "key2", "key3"}
items, err := client.GetBulkState(ctx, store, keys, nil,100)

以及 ExecuteStateTransaction 方法,用于以事务方式执行多个插入或删除操作。

ops := make([]*dapr.StateOperation, 0)

op1 := &dapr.StateOperation{
    Type: dapr.StateOperationTypeUpsert,
    Item: &dapr.SetStateItem{
        Key:   "key1",
        Value: []byte(data),
    },
}
op2 := &dapr.StateOperation{
    Type: dapr.StateOperationTypeDelete,
    Item: &dapr.SetStateItem{
        Key:   "key2",
    },
}
ops = append(ops, op1, op2)
meta := map[string]string{}
err := testClient.ExecuteStateTransaction(ctx, store, meta, ops)

使用 QueryState 检索、过滤和排序存储在状态存储中的键/值数据。

// 定义查询字符串
query := `{
	"filter": {
		"EQ": { "value.Id": "1" }
	},
	"sort": [
		{
			"key": "value.Balance",
			"order": "DESC"
		}
	]
}`

// 使用客户端查询状态
queryResponse, err := c.QueryState(ctx, "querystore", query)
if err != nil {
	log.Fatal(err)
}

fmt.Printf("Got %d\n", len(queryResponse))

for _, account := range queryResponse {
	var data Account
	err := account.Unmarshal(&data)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Account: %s has %f\n", data.ID, data.Balance)
}

注意: 查询状态 API 目前处于 alpha 阶段

有关状态管理的完整指南,请访问 如何保存和获取状态

发布消息

要将数据发布到主题上,Dapr Go 客户端提供了一个简单的方法:

data := []byte(`{ "id": "a123", "value": "abcdefg", "valid": true }`)
if err := client.PublishEvent(ctx, "component-name", "topic-name", data); err != nil {
    panic(err)
}

要一次发布多个消息,可以使用 PublishEvents 方法:

events := []string{"event1", "event2", "event3"}
res := client.PublishEvents(ctx, "component-name", "topic-name", events)
if res.Error != nil {
    panic(res.Error)
}

有关发布/订阅的完整指南,请访问 如何发布和订阅

工作流

您可以使用 Go SDK 创建 工作流。例如,从一个简单的工作流活动开始:

func TestActivity(ctx workflow.ActivityContext) (any, error) {
	var input int
	if err := ctx.GetInput(&input); err != nil {
		return "", err
	}
	
	// 在这里做一些事情
	return "result", nil
}

编写一个简单的工作流函数:

func TestWorkflow(ctx *workflow.WorkflowContext) (any, error) {
	var input int
	if err := ctx.GetInput(&input); err != nil {
		return nil, err
	}
	var output string
	if err := ctx.CallActivity(TestActivity, workflow.ActivityInput(input)).Await(&output); err != nil {
		return nil, err
	}
	if err := ctx.WaitForExternalEvent("testEvent", time.Second*60).Await(&output); err != nil {
		return nil, err
	}
	
	if err := ctx.CreateTimer(time.Second).Await(nil); err != nil {
		return nil, nil
	}
	return output, nil
}

然后编写将使用您创建的工作流的应用程序。有关完整的演练,请参阅 如何编写工作流指南

尝试 Go SDK 工作流示例

输出绑定

Dapr Go 客户端 SDK 提供了两种方法来调用 Dapr 定义的绑定上的操作。Dapr 支持输入、输出和双向绑定。

对于简单的输出绑定:

in := &dapr.InvokeBindingRequest{ Name: "binding-name", Operation: "operation-name" }
err = client.InvokeOutputBinding(ctx, in)

调用带内容和元数据的方法:

in := &dapr.InvokeBindingRequest{
    Name:      "binding-name",
    Operation: "operation-name",
    Data: []byte("hello"),
    Metadata: map[string]string{"k1": "v1", "k2": "v2"},
}

out, err := client.InvokeBinding(ctx, in)

有关输出绑定的完整指南,请访问 如何使用绑定

Actor

使用 Dapr Go 客户端 SDK 编写 actor。

// MyActor 表示一个示例 actor 类型。
type MyActor struct {
	actors.Actor
}

// MyActorMethod 是可以在 MyActor 上调用的方法。
func (a *MyActor) MyActorMethod(ctx context.Context, req *actors.Message) (string, error) {
	log.Printf("Received message: %s", req.Data)
	return "Hello from MyActor!", nil
}

func main() {
	// 创建一个 Dapr 客户端
	daprClient, err := client.NewClient()
	if err != nil {
		log.Fatal("Error creating Dapr client: ", err)
	}

	// 向 Dapr 注册 actor 类型
	actors.RegisterActor(&MyActor{})

	// 创建一个 actor 客户端
	actorClient := actors.NewClient(daprClient)

	// 创建一个 actor ID
	actorID := actors.NewActorID("myactor")

	// 获取或创建 actor
	err = actorClient.SaveActorState(context.Background(), "myactorstore", actorID, map[string]interface{}{"data": "initial state"})
	if err != nil {
		log.Fatal("Error saving actor state: ", err)
	}

	// 调用 actor 上的方法
	resp, err := actorClient.InvokeActorMethod(context.Background(), "myactorstore", actorID, "MyActorMethod", &actors.Message{Data: []byte("Hello from client!")})
	if err != nil {
		log.Fatal("Error invoking actor method: ", err)
	}

	log.Printf("Response from actor: %s", resp.Data)

	// 在终止前等待几秒钟
	time.Sleep(5 * time.Second)

	// 删除 actor
	err = actorClient.DeleteActor(context.Background(), "myactorstore", actorID)
	if err != nil {
		log.Fatal("Error deleting actor: ", err)
	}

	// 关闭 Dapr 客户端
	daprClient.Close()
}

有关 actor 的完整指南,请访问 actor 构建块文档

Secret 管理

Dapr 客户端还提供对运行时 secret 的访问,这些 secret 可以由任意数量的 secret 存储(例如 Kubernetes Secrets、HashiCorp Vault 或 Azure KeyVault)支持:

opt := map[string]string{
    "version": "2",
}

secret, err := client.GetSecret(ctx, "store-name", "secret-name", opt)

认证

默认情况下,Dapr 依赖于网络边界来限制对其 API 的访问。然而,如果目标 Dapr API 配置了基于令牌的认证,用户可以通过两种方式配置 Go Dapr 客户端以使用该令牌:

环境变量

如果定义了 DAPR_API_TOKEN 环境变量,Dapr 将自动使用它来增强其 Dapr API 调用以确保认证。

显式方法

此外,用户还可以在任何 Dapr 客户端实例上显式设置 API 令牌。这种方法在用户代码需要为不同的 Dapr API 端点创建多个客户端时非常有用。

func main() {
    client, err := dapr.NewClient()
    if err != nil {
        panic(err)
    }
    defer client.Close()
    client.WithAuthToken("your-Dapr-API-token-here")
}

有关 secret 的完整指南,请访问 如何检索 secret

分布式锁

Dapr 客户端提供了使用锁对资源的互斥访问。通过锁,您可以:

  • 提供对数据库行、表或整个数据库的访问
  • 以顺序方式锁定从队列中读取消息
package main

import (
    "fmt"

    dapr "github.com/dapr/go-sdk/client"
)

func main() {
    client, err := dapr.NewClient()
    if err != nil {
        panic(err)
    }
    defer client.Close()
    
    resp, err := client.TryLockAlpha1(ctx, "lockstore", &dapr.LockRequest{
			LockOwner:         "random_id_abc123",
			ResourceID:      "my_file_name",
			ExpiryInSeconds: 60,
		})

    fmt.Println(resp.Success)
}

有关分布式锁的完整指南,请访问 如何使用锁

配置

使用 Dapr 客户端 Go SDK,您可以消费作为只读键/值对返回的配置项,并订阅配置项的更改。

配置获取

	items, err := client.GetConfigurationItem(ctx, "example-config", "mykey")
	if err != nil {
		panic(err)
	}
	fmt.Printf("get config = %s\n", (*items).Value)

配置订阅

go func() {
	if err := client.SubscribeConfigurationItems(ctx, "example-config", []string{"mySubscribeKey1", "mySubscribeKey2", "mySubscribeKey3"}, func(id string, items map[string]*dapr.ConfigurationItem) {
		for k, v := range items {
			fmt.Printf("get updated config key = %s, value = %s \n", k, v.Value)
		}
		subscribeID = id
	}); err != nil {
		panic(err)
	}
}()

有关配置的完整指南,请访问 如何从存储管理配置

加密

使用 Dapr 客户端 Go SDK,您可以使用高级 EncryptDecrypt 加密 API 在处理数据流时加密和解密文件。

加密:

// 使用 Dapr 加密数据
out, err := client.Encrypt(context.Background(), rf, dapr.EncryptOptions{
	// 这是 3 个必需的参数
	ComponentName: "mycryptocomponent",
	KeyName:        "mykey",
	Algorithm:     "RSA",
})
if err != nil {
	panic(err)
}

解密:

// 使用 Dapr 解密数据
out, err := client.Decrypt(context.Background(), rf, dapr.EncryptOptions{
	// 唯一必需的选项是组件名称
	ComponentName: "mycryptocomponent",
})

有关加密的完整指南,请访问 如何使用加密 API

相关链接

Go SDK 示例

3 - Dapr Java SDK

Java SDK包,用于开发Dapr应用

Dapr 提供多种包以帮助开发 Java 应用程序。通过这些包,您可以使用 Dapr 创建 Java 客户端、服务器和虚拟 actor。

前提条件

导入 Dapr Java SDK

接下来,导入 Java SDK 包以开始使用。选择您喜欢的构建工具以了解如何导入。

对于 Maven 项目,将以下内容添加到您的 pom.xml 文件中:

<project>
  ...
  <dependencies>
    ...
    <!-- Dapr 的核心 SDK,包含所有功能,actor 除外。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk</artifactId>
      <version>1.13.1</version>
    </dependency>
    <!-- Dapr 的 actor SDK(可选)。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk-actors</artifactId>
      <version>1.13.1</version>
    </dependency>
    <!-- Dapr 与 SpringBoot 的 SDK 集成(可选)。 -->
    <dependency>
      <groupId>io.dapr</groupId>
      <artifactId>dapr-sdk-springboot</artifactId>
      <version>1.13.1</version>
    </dependency>
    ...
  </dependencies>
  ...
</project>

对于 Gradle 项目,将以下内容添加到您的 build.gradle 文件中:

dependencies {
...
    // Dapr 的核心 SDK,包含所有功能,actor 除外。
    compile('io.dapr:dapr-sdk:1.13.1')
    // Dapr 的 actor SDK(可选)。
    compile('io.dapr:dapr-sdk-actors:1.13.1')
    // Dapr 与 SpringBoot 的 SDK 集成(可选)。
    compile('io.dapr:dapr-sdk-springboot:1.13.1')
}

如果您也在使用 Spring Boot,可能会遇到一个常见问题,即 Dapr SDK 使用的 OkHttp 版本与 Spring Boot Bill of Materials 中指定的版本冲突。

您可以通过在项目中指定与 Dapr SDK 使用的版本兼容的 OkHttp 版本来解决此问题:

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>1.13.1</version>
</dependency>

试用

测试 Dapr Java SDK。通过 Java 快速入门和教程来查看 Dapr 的实际应用:

SDK 示例描述
快速入门使用 Java SDK 在几分钟内体验 Dapr 的 API 构建块。
SDK 示例克隆 SDK 仓库以尝试一些示例并开始使用。
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  // 发送带有消息的类;BINDING_OPERATION="create"
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, myClass).block();

  // 发送纯字符串
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, message).block();
}

可用包

客户端

创建与 Dapr sidecar 和其他 Dapr 应用程序交互的 Java 客户端。

工作流

创建和管理与其他 Dapr API 一起使用的工作流。

3.1 - 工作流

如何使用 Dapr 工作流扩展快速启动和运行

在这篇文档中,我们将介绍如何使用 Dapr 工作流扩展来快速启动和运行工作流。Dapr 工作流扩展为定义和管理工作流提供了一种简便的方法,使开发者能够轻松地在分布式系统中编排复杂的业务流程。

Dapr 工作流扩展的主要功能包括:

  • 工作流定义:可以使用 YAML 或 JSON 格式来定义工作流。
  • 状态管理:通过 Dapr 的状态管理功能,确保工作流在执行过程中状态的正确维护。
  • 服务调用:利用 Dapr 的服务调用功能,在工作流中与其他服务进行交互。
  • 事件驱动:通过发布/订阅机制,工作流可以响应事件并触发相应的操作。
  • 定时器和提醒:使用定时器和提醒功能,在工作流中设置定时任务和提醒。

通过这些功能,Dapr 工作流扩展能够帮助开发者更高效地构建和管理分布式应用程序中的工作流。

3.1.1 - 如何:在 Java SDK 中编写和管理 Dapr 工作流

如何使用 Dapr Java SDK 快速启动和运行工作流

我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的工作流示例,您将:

自托管模式下,此示例使用 dapr init 的默认配置运行。

准备工作

  • 确保您使用的是最新版本的 proto 绑定

设置环境

克隆 Java SDK 仓库并进入其中。

git clone https://github.com/dapr/java-sdk.git
cd java-sdk

运行以下命令以安装运行此工作流示例所需的 Dapr Java SDK 依赖项。

mvn clean install

从 Java SDK 根目录,导航到 Dapr 工作流示例。

cd examples

运行 DemoWorkflowWorker

DemoWorkflowWorker 类在 Dapr 的工作流运行时引擎中注册了 DemoWorkflow 的实现。在 DemoWorkflowWorker.java 文件中,您可以找到 DemoWorkflowWorker 类和 main 方法:

public class DemoWorkflowWorker {

  public static void main(String[] args) throws Exception {
    // Register the Workflow with the runtime.
    WorkflowRuntime.getInstance().registerWorkflow(DemoWorkflow.class);
    System.out.println("Start workflow runtime");
    WorkflowRuntime.getInstance().startAndBlock();
    System.exit(0);
  }
}

在上面的代码中:

  • WorkflowRuntime.getInstance().registerWorkflow()DemoWorkflow 注册为 Dapr 工作流运行时中的一个工作流。
  • WorkflowRuntime.getInstance().start() 在 Dapr 工作流运行时中构建并启动引擎。

在终端中,执行以下命令以启动 DemoWorkflowWorker

dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowWorker

预期输出

You're up and running! Both Dapr and your app logs will appear here.

...

== APP == Start workflow runtime
== APP == Sep 13, 2023 9:02:03 AM com.microsoft.durabletask.DurableTaskGrpcWorker startAndBlock
== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001.

运行 DemoWorkflowClient

DemoWorkflowClient 启动已在 Dapr 中注册的工作流实例。

public class DemoWorkflowClient {

  // ...
  public static void main(String[] args) throws InterruptedException {
    DaprWorkflowClient client = new DaprWorkflowClient();

    try (client) {
      String separatorStr = "*******";
      System.out.println(separatorStr);
      String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
      System.out.printf("Started new workflow instance with random ID: %s%n", instanceId);

      System.out.println(separatorStr);
      System.out.println("**GetInstanceMetadata:Running Workflow**");
      WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
      System.out.printf("Result: %s%n", workflowMetadata);

      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceStart**");
      try {
        WorkflowInstanceStatus waitForInstanceStartResult =
            client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceStartResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceStart has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**SendExternalMessage**");
      client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");

      System.out.println(separatorStr);
      System.out.println("** Registering parallel Events to be captured by allOf(t1,t2,t3) **");
      client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
      client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
      client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
      System.out.printf("Events raised for workflow with instanceId: %s\n", instanceId);

      System.out.println(separatorStr);
      System.out.println("** Registering Event to be captured by anyOf(t1,t2,t3) **");
      client.raiseEvent(instanceId, "e2", "event 2 Payload");
      System.out.printf("Event raised for workflow with instanceId: %s\n", instanceId);

      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceCompletion**");
      try {
        WorkflowInstanceStatus waitForInstanceCompletionResult =
            client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceCompletionResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceCompletion has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**purgeInstance**");
      boolean purgeResult = client.purgeInstance(instanceId);
      System.out.printf("purgeResult: %s%n", purgeResult);

      System.out.println(separatorStr);
      System.out.println("**raiseEvent**");

      String eventInstanceId = client.scheduleNewWorkflow(DemoWorkflow.class);
      System.out.printf("Started new workflow instance with random ID: %s%n", eventInstanceId);
      client.raiseEvent(eventInstanceId, "TestException", null);
      System.out.printf("Event raised for workflow with instanceId: %s\n", eventInstanceId);

      System.out.println(separatorStr);
      String instanceToTerminateId = "terminateMe";
      client.scheduleNewWorkflow(DemoWorkflow.class, null, instanceToTerminateId);
      System.out.printf("Started new workflow instance with specified ID: %s%n", instanceToTerminateId);

      TimeUnit.SECONDS.sleep(5);
      System.out.println("Terminate this workflow instance manually before the timeout is reached");
      client.terminateWorkflow(instanceToTerminateId, null);
      System.out.println(separatorStr);

      String restartingInstanceId = "restarting";
      client.scheduleNewWorkflow(DemoWorkflow.class, null, restartingInstanceId);
      System.out.printf("Started new  workflow instance with ID: %s%n", restartingInstanceId);
      System.out.println("Sleeping 30 seconds to restart the workflow");
      TimeUnit.SECONDS.sleep(30);

      System.out.println("**SendExternalMessage: RestartEvent**");
      client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");

      System.out.println("Sleeping 30 seconds to terminate the eternal workflow");
      TimeUnit.SECONDS.sleep(30);
      client.terminateWorkflow(restartingInstanceId, null);
    }

    System.out.println("Exiting DemoWorkflowClient.");
    System.exit(0);
  }
}

在第二个终端窗口中,通过运行以下命令启动工作流:

java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.DemoWorkflowClient

预期输出

*******
Started new workflow instance with random ID: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**GetInstanceMetadata:Running Workflow**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**WaitForInstanceStart**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: RUNNING, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:30.699Z, Input: '"input data"', Output: '']
*******
**SendExternalMessage**
*******
** Registering parallel Events to be captured by allOf(t1,t2,t3) **
Events raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
** Registering Event to be captured by anyOf(t1,t2,t3) **
Event raised for workflow with instanceId: 0b4cc0d5-413a-4c1c-816a-a71fa24740d4
*******
**WaitForInstanceCompletion**
Result: [Name: 'io.dapr.examples.workflows.DemoWorkflow', ID: '0b4cc0d5-413a-4c1c-816a-a71fa24740d4', RuntimeStatus: FAILED, CreatedAt: 2023-09-13T13:02:30.547Z, LastUpdatedAt: 2023-09-13T13:02:55.054Z, Input: '"input data"', Output: '']
*******
**purgeInstance**
purgeResult: true
*******
**raiseEvent**
Started new workflow instance with random ID: 7707d141-ebd0-4e54-816e-703cb7a52747
Event raised for workflow with instanceId: 7707d141-ebd0-4e54-816e-703cb7a52747
*******
Started new workflow instance with specified ID: terminateMe
Terminate this workflow instance manually before the timeout is reached
*******
Started new  workflow instance with ID: restarting
Sleeping 30 seconds to restart the workflow
**SendExternalMessage: RestartEvent**
Sleeping 30 seconds to terminate the eternal workflow
Exiting DemoWorkflowClient.

发生了什么?

  1. 当您运行 dapr run 时,工作流工作者将工作流(DemoWorkflow)及其活动注册到 Dapr 工作流引擎。
  2. 当您运行 java 时,工作流客户端启动了具有以下活动的工作流实例。您可以在运行 dapr run 的终端中查看输出。
    1. 工作流启动,触发三个并行任务,并等待它们完成。
    2. 工作流客户端调用活动并将 “Hello Activity” 消息发送到控制台。
    3. 工作流超时并被清除。
    4. 工作流客户端启动一个具有随机 ID 的新工作流实例,使用另一个名为 terminateMe 的工作流实例终止它,并使用名为 restarting 的工作流重新启动它。
    5. 然后工作流客户端退出。

下一步

3.2 - 使用 Dapr 客户端 Java SDK 入门

如何使用 Dapr Java SDK 快速上手

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

前提条件

完成初始设置并将 Java SDK 导入您的项目

初始化客户端

您可以这样初始化 Dapr 客户端:

DaprClient client = new DaprClientBuilder().build();

这会连接到默认的 Dapr gRPC 端点 localhost:50001

环境变量

Dapr Sidecar 端点

您可以使用标准化的 DAPR_GRPC_ENDPOINTDAPR_HTTP_ENDPOINT 环境变量来指定不同的 gRPC 或 HTTP 端点。当设置了这些变量时,客户端将自动使用它们连接到 Dapr sidecar。

旧的环境变量 DAPR_HTTP_PORTDAPR_GRPC_PORT 仍然受支持,但 DAPR_GRPC_ENDPOINTDAPR_HTTP_ENDPOINT 优先。

Dapr API 令牌

如果您的 Dapr 实例需要 DAPR_API_TOKEN 环境变量,您可以在环境中设置,客户端会自动使用。
您可以在这里阅读更多关于 Dapr API 令牌认证的信息。

错误处理

最初,Dapr 中的错误遵循标准 gRPC 错误模型。为了提供更详细的信息,在 1.13 版本中引入了一个增强的错误模型,与 gRPC 的丰富错误模型对齐。Java SDK 扩展了 DaprException,以包含 Dapr 中添加的错误详细信息。

使用 Dapr Java SDK 处理 DaprException 并消费错误详细信息的示例:

...
      try {
        client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
      } catch (DaprException exception) {
        System.out.println("Dapr 异常的错误代码: " + exception.getErrorCode());
        System.out.println("Dapr 异常的消息: " + exception.getMessage());
        // DaprException 现在通过 `getStatusDetails()` 提供来自 Dapr 运行时的更多错误详细信息。
        System.out.println("Dapr 异常的原因: " + exception.getStatusDetails().get(
        DaprErrorDetails.ErrorDetailType.ERROR_INFO,
            "reason",
        TypeRef.STRING));
      }
...

构建块

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

调用服务

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  // 调用 'GET' 方法 (HTTP) 跳过序列化: 返回类型为 Mono<byte[]>
  // 对于 gRPC 设置 HttpExtension.NONE 参数
  response = client.invokeMethod(SERVICE_TO_INVOKE, METHOD_TO_INVOKE, "{\"name\":\"World!\"}", HttpExtension.GET, byte[].class).block();

  // 调用 'POST' 方法 (HTTP) 跳过序列化: 返回类型为 Mono<byte[]>     
  response = client.invokeMethod(SERVICE_TO_INVOKE, METHOD_TO_INVOKE, "{\"id\":\"100\", \"FirstName\":\"Value\", \"LastName\":\"Value\"}", HttpExtension.POST, byte[].class).block();

  System.out.println(new String(response));

  // 调用 'POST' 方法 (HTTP) 带序列化: 返回类型为 Mono<Employee>      
  Employee newEmployee = new Employee("Nigel", "Guitarist");
  Employee employeeResponse = client.invokeMethod(SERVICE_TO_INVOKE, "employees", newEmployee, HttpExtension.POST, Employee.class).block();
}

保存和获取应用程序状态

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.State;
import reactor.core.publisher.Mono;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  // 保存状态
  client.saveState(STATE_STORE_NAME, FIRST_KEY_NAME, myClass).block();

  // 获取状态
  State<MyClass> retrievedMessage = client.getState(STATE_STORE_NAME, FIRST_KEY_NAME, MyClass.class).block();

  // 删除状态
  client.deleteState(STATE_STORE_NAME, FIRST_KEY_NAME).block();
}

发布和订阅消息

发布消息
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.domain.Metadata;
import static java.util.Collections.singletonMap;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  client.publishEvent(PUBSUB_NAME, TOPIC_NAME, message, singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)).block();
}
订阅消息
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.Topic;
import io.dapr.client.domain.BulkSubscribeAppResponse;
import io.dapr.client.domain.BulkSubscribeAppResponseEntry;
import io.dapr.client.domain.BulkSubscribeAppResponseStatus;
import io.dapr.client.domain.BulkSubscribeMessage;
import io.dapr.client.domain.BulkSubscribeMessageEntry;
import io.dapr.client.domain.CloudEvent;
import io.dapr.springboot.annotations.BulkSubscribe;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class SubscriberController {

  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

  @Topic(name = "testingtopic", pubsubName = "${myAppProperty:messagebus}")
  @PostMapping(path = "/testingtopic")
  public Mono<Void> handleMessage(@RequestBody(required = false) CloudEvent<?> cloudEvent) {
    return Mono.fromRunnable(() -> {
      try {
        System.out.println("Subscriber got: " + cloudEvent.getData());
        System.out.println("Subscriber got: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    });
  }

  @Topic(name = "testingtopic", pubsubName = "${myAppProperty:messagebus}",
          rule = @Rule(match = "event.type == 'myevent.v2'", priority = 1))
  @PostMapping(path = "/testingtopicV2")
  public Mono<Void> handleMessageV2(@RequestBody(required = false) CloudEvent envelope) {
    return Mono.fromRunnable(() -> {
      try {
        System.out.println("Subscriber got: " + cloudEvent.getData());
        System.out.println("Subscriber got: " + OBJECT_MAPPER.writeValueAsString(cloudEvent));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    });
  }

  @BulkSubscribe()
  @Topic(name = "testingtopicbulk", pubsubName = "${myAppProperty:messagebus}")
  @PostMapping(path = "/testingtopicbulk")
  public Mono<BulkSubscribeAppResponse> handleBulkMessage(
          @RequestBody(required = false) BulkSubscribeMessage<CloudEvent<String>> bulkMessage) {
    return Mono.fromCallable(() -> {
      if (bulkMessage.getEntries().size() == 0) {
        return new BulkSubscribeAppResponse(new ArrayList<BulkSubscribeAppResponseEntry>());
      }

      System.out.println("Bulk Subscriber received " + bulkMessage.getEntries().size() + " messages.");

      List<BulkSubscribeAppResponseEntry> entries = new ArrayList<BulkSubscribeAppResponseEntry>();
      for (BulkSubscribeMessageEntry<?> entry : bulkMessage.getEntries()) {
        try {
          System.out.printf("Bulk Subscriber message has entry ID: %s\n", entry.getEntryId());
          CloudEvent<?> cloudEvent = (CloudEvent<?>) entry.getEvent();
          System.out.printf("Bulk Subscriber got: %s\n", cloudEvent.getData());
          entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.SUCCESS));
        } catch (Exception e) {
          e.printStackTrace();
          entries.add(new BulkSubscribeAppResponseEntry(entry.getEntryId(), BulkSubscribeAppResponseStatus.RETRY));
        }
      }
      return new BulkSubscribeAppResponse(entries);
    });
  }
}
批量发布消息

注意: API 处于 Alpha 阶段

import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.BulkPublishResponse;
import io.dapr.client.domain.BulkPublishResponseFailedEntry;
import java.util.ArrayList;
import java.util.List;
class Solution {
  public void publishMessages() {
    try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
      // 创建要发布的消息列表
      List<String> messages = new ArrayList<>();
      for (int i = 0; i < NUM_MESSAGES; i++) {
        String message = String.format("This is message #%d", i);
        messages.add(message);
        System.out.println("Going to publish message : " + message);
      }

      // 使用批量发布 API 发布消息列表
      BulkPublishResponse<String> res = client.publishEvents(PUBSUB_NAME, TOPIC_NAME, "text/plain", messages).block()
    }
  }
}

与输出绑定交互

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  // 发送带有消息的类; BINDING_OPERATION="create"
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, myClass).block();

  // 发送纯字符串
  client.invokeBinding(BINDING_NAME, BINDING_OPERATION, message).block();
}

与输入绑定交互

import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
@RequestMapping("/")
public class myClass {
    private static final Logger log = LoggerFactory.getLogger(myClass);
        @PostMapping(path = "/checkout")
        public Mono<String> getCheckout(@RequestBody(required = false) byte[] body) {
            return Mono.fromRunnable(() ->
                    log.info("Received Message: " + new String(body)));
        }
}

检索秘密

import com.fasterxml.jackson.databind.ObjectMapper;
importio.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import java.util.Map;

try (DaprClient client = (new DaprClientBuilder()).build()) {
  Map<String, String> secret = client.getSecret(SECRET_STORE_NAME, secretKey).block();
  System.out.println(JSON_SERIALIZER.writeValueAsString(secret));
}

Actors

actor 是一个具有单线程执行的隔离、独立的计算和状态单元。Dapr 提供了一种基于 虚拟 actor 模式 的 actor 实现,该模式提供了单线程编程模型,并且当 actor 不在使用时会被垃圾回收。使用 Dapr 的实现,您可以根据 actor 模型编写 Dapr actor,Dapr 利用底层平台提供的可扩展性和可靠性。

import io.dapr.actors.ActorMethod;
import io.dapr.actors.ActorType;
import reactor.core.publisher.Mono;

@ActorType(name = "DemoActor")
public interface DemoActor {

  void registerReminder();

  @ActorMethod(name = "echo_message")
  String say(String something);

  void clock(String message);

  @ActorMethod(returns = Integer.class)
  Mono<Integer> incrementAndGet(int delta);
}

获取和订阅应用程序配置

注意这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问

import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.ConfigurationItem;
import io.dapr.client.domain.GetConfigurationRequest;
import io.dapr.client.domain.SubscribeConfigurationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
  // 获取单个键的配置
  Mono<ConfigurationItem> item = client.getConfiguration(CONFIG_STORE_NAME, CONFIG_KEY).block();

  // 获取多个键的配置
  Mono<Map<String, ConfigurationItem>> items =
          client.getConfiguration(CONFIG_STORE_NAME, CONFIG_KEY_1, CONFIG_KEY_2);

  // 订阅配置更改
  Flux<SubscribeConfigurationResponse> outFlux = client.subscribeConfiguration(CONFIG_STORE_NAME, CONFIG_KEY_1, CONFIG_KEY_2);
  outFlux.subscribe(configItems -> configItems.forEach(...));

  // 取消订阅配置更改
  Mono<UnsubscribeConfigurationResponse> unsubscribe = client.unsubscribeConfiguration(SUBSCRIPTION_ID, CONFIG_STORE_NAME)
}

查询保存的状态

注意这是一个预览 API,因此只能通过 DaprPreviewClient 接口访问,而不能通过普通的 DaprClient 接口访问

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.QueryStateItem;
import io.dapr.client.domain.QueryStateRequest;
import io.dapr.client.domain.QueryStateResponse;
import io.dapr.client.domain.query.Query;
import io.dapr.client.domain.query.Sorting;
import io.dapr.client.domain.query.filters.EqFilter;

try (DaprClient client = builder.build(); DaprPreviewClient previewClient = builder.buildPreviewClient()) {
        String searchVal = args.length == 0 ? "searchValue" : args[0];
        
        // 创建 JSON 数据
        Listing first = new Listing();
        first.setPropertyType("apartment");
        first.setId("1000");
        ...
        Listing second = new Listing();
        second.setPropertyType("row-house");
        second.setId("1002");
        ...
        Listing third = new Listing();
        third.setPropertyType("apartment");
        third.setId("1003");
        ...
        Listing fourth = new Listing();
        fourth.setPropertyType("apartment");
        fourth.setId("1001");
        ...
        Map<String, String> meta = new HashMap<>();
        meta.put("contentType", "application/json");

        // 保存状态
        SaveStateRequest request = new SaveStateRequest(STATE_STORE_NAME).setStates(
          new State<>("1", first, null, meta, null),
          new State<>("2", second, null, meta, null),
          new State<>("3", third, null, meta, null),
          new State<>("4", fourth, null, meta, null)
        );
        client.saveBulkState(request).block();
        
        
        // 创建查询和查询状态请求

        Query query = new Query()
          .setFilter(new EqFilter<>("propertyType", "apartment"))
          .setSort(Arrays.asList(new Sorting("id", Sorting.Order.DESC)));
        QueryStateRequest request = new QueryStateRequest(STATE_STORE_NAME)
          .setQuery(query);

        // 使用预览客户端调用查询状态 API
        QueryStateResponse<MyData> result = previewClient.queryState(request, MyData.class).block();
        
        // 查看查询状态响应 
        System.out.println("Found " + result.getResults().size() + " items.");
        for (QueryStateItem<Listing> item : result.getResults()) {
          System.out.println("Key: " + item.getKey());
          System.out.println("Data: " + item.getValue());
        }
}

分布式锁

package io.dapr.examples.lock.grpc;

import io.dapr.client.DaprClientBuilder;
import io.dapr.client.DaprPreviewClient;
import io.dapr.client.domain.LockRequest;
import io.dapr.client.domain.UnlockRequest;
import io.dapr.client.domain.UnlockResponseStatus;
import reactor.core.publisher.Mono;

public class DistributedLockGrpcClient {
  private static final String LOCK_STORE_NAME = "lockstore";

  /**
   * 执行各种方法以检查不同的 API。
   *
   * @param args 参数
   * @throws Exception 抛出异常
   */
  public static void main(String[] args) throws Exception {
    try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) {
      System.out.println("Using preview client...");
      tryLock(client);
      unlock(client);
    }
  }

  /**
   * 尝试获取锁。
   *
   * @param client DaprPreviewClient 对象
   */
  public static void tryLock(DaprPreviewClient client) {
    System.out.println("*******尝试获取一个空闲的分布式锁********");
    try {
      LockRequest lockRequest = new LockRequest(LOCK_STORE_NAME, "resouce1", "owner1", 5);
      Mono<Boolean> result = client.tryLock(lockRequest);
      System.out.println("Lock result -> " + (Boolean.TRUE.equals(result.block()) ? "SUCCESS" : "FAIL"));
    } catch (Exception ex) {
      System.out.println(ex.getMessage());
    }
  }

  /**
   * 解锁。
   *
   * @param client DaprPreviewClient 对象
   */
  public static void unlock(DaprPreviewClient client) {
    System.out.println("*******解锁一个分布式锁********");
    try {
      UnlockRequest unlockRequest = new UnlockRequest(LOCK_STORE_NAME, "resouce1", "owner1");
      Mono<UnlockResponseStatus> result = client.unlock(unlockRequest);
      System.out.println("Unlock result ->" + result.block().name());
    } catch (Exception ex) {
      System.out.println(ex.getMessage());
    }
  }
}

工作流

package io.dapr.examples.workflows;

import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * 有关设置说明,请参阅 README。
 */
public class DemoWorkflowClient {

  /**
   * 主方法。
   *
   * @param args 输入参数(未使用)。
   * @throws InterruptedException 如果程序被中断。
   */
  public static void main(String[] args) throws InterruptedException {
    DaprWorkflowClient client = new DaprWorkflowClient();

    try (client) {
      String separatorStr = "*******";
      System.out.println(separatorStr);
      String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class, "input data");
      System.out.printf("Started new workflow instance with random ID: %s%n", instanceId);

      System.out.println(separatorStr);
      System.out.println("**GetInstanceMetadata:Running Workflow**");
      WorkflowInstanceStatus workflowMetadata = client.getInstanceState(instanceId, true);
      System.out.printf("Result: %s%n", workflowMetadata);

      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceStart**");
      try {
        WorkflowInstanceStatus waitForInstanceStartResult =
            client.waitForInstanceStart(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceStartResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceStart has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**SendExternalMessage**");
      client.raiseEvent(instanceId, "TestEvent", "TestEventPayload");

      System.out.println(separatorStr);
      System.out.println("** Registering parallel Events to be captured by allOf(t1,t2,t3) **");
      client.raiseEvent(instanceId, "event1", "TestEvent 1 Payload");
      client.raiseEvent(instanceId, "event2", "TestEvent 2 Payload");
      client.raiseEvent(instanceId, "event3", "TestEvent 3 Payload");
      System.out.printf("Events raised for workflow with instanceId: %s\n", instanceId);

      System.out.println(separatorStr);
      System.out.println("** Registering Event to be captured by anyOf(t1,t2,t3) **");
      client.raiseEvent(instanceId, "e2", "event 2 Payload");
      System.out.printf("Event raised for workflow with instanceId: %s\n", instanceId);


      System.out.println(separatorStr);
      System.out.println("**WaitForInstanceCompletion**");
      try {
        WorkflowInstanceStatus waitForInstanceCompletionResult =
            client.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true);
        System.out.printf("Result: %s%n", waitForInstanceCompletionResult);
      } catch (TimeoutException ex) {
        System.out.printf("waitForInstanceCompletion has an exception:%s%n", ex);
      }

      System.out.println(separatorStr);
      System.out.println("**purgeInstance**");
      boolean purgeResult = client.purgeInstance(instanceId);
      System.out.printf("purgeResult: %s%n", purgeResult);

      System.out.println(separatorStr);
      System.out.println("**raiseEvent**");

      String eventInstanceId = client.scheduleNewWorkflow(DemoWorkflow.class);
      System.out.printf("Started new workflow instance with random ID: %s%n", eventInstanceId);
      client.raiseEvent(eventInstanceId, "TestException", null);
      System.out.printf("Event raised for workflow with instanceId: %s\n", eventInstanceId);

      System.out.println(separatorStr);
      String instanceToTerminateId = "terminateMe";
      client.scheduleNewWorkflow(DemoWorkflow.class, null, instanceToTerminateId);
      System.out.printf("Started new workflow instance with specified ID: %s%n", instanceToTerminateId);

      TimeUnit.SECONDS.sleep(5);
      System.out.println("Terminate this workflow instance manually before the timeout is reached");
      client.terminateWorkflow(instanceToTerminateId, null);
      System.out.println(separatorStr);

      String restartingInstanceId = "restarting";
      client.scheduleNewWorkflow(DemoWorkflow.class, null, restartingInstanceId);
      System.out.printf("Started new  workflow instance with ID: %s%n", restartingInstanceId);
      System.out.println("Sleeping 30 seconds to restart the workflow");
      TimeUnit.SECONDS.sleep(30);

      System.out.println("**SendExternalMessage: RestartEvent**");
      client.raiseEvent(restartingInstanceId, "RestartEvent", "RestartEventPayload");

      System.out.println("Sleeping 30 seconds to terminate the eternal workflow");
      TimeUnit.SECONDS.sleep(30);
      client.terminateWorkflow(restartingInstanceId, null);
    }

    System.out.println("Exiting DemoWorkflowClient.");
    System.exit(0);
  }
}

Sidecar API

等待 sidecar

DaprClient 还提供了一个辅助方法来等待 sidecar 变得健康(仅限组件)。使用此方法时,请确保指定超时时间(以毫秒为单位)并使用 block() 来等待反应操作的结果。

// 在尝试使用 Dapr 组件之前,等待 Dapr sidecar 报告健康。
try (DaprClient client = new DaprClientBuilder().build()) {
  System.out.println("Waiting for Dapr sidecar ...");
  client.waitForSidecar(10000).block(); // 指定超时时间(以毫秒为单位)
  System.out.println("Dapr sidecar is ready.");
  ...
}

// 在此处执行 Dapr 组件操作,例如获取秘密或保存状态。

关闭 sidecar

try (DaprClient client = new DaprClientBuilder().build()) {
  logger.info("Sending shutdown request.");
  client.shutdown().block();
  logger.info("Ensuring dapr has stopped.");
  ...
}

了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包

相关链接

3.3 - 开始使用 Dapr 和 Spring Boot

如何开始使用 Dapr 和 Spring Boot

通过将 Dapr 和 Spring Boot 结合使用,我们可以创建不依赖于特定基础设施的 Java 应用程序,这些应用程序可以部署在不同的环境中,并支持多种本地和云服务提供商。

首先,我们将从一个简单的集成开始,涵盖 DaprClientTestcontainers 的集成,然后利用 Spring 和 Spring Boot 的机制及编程模型来使用 Dapr API。这有助于团队消除连接到特定环境基础设施(如数据库、键值存储、消息代理、配置/密钥存储等)所需的客户端和驱动程序等依赖项。

将 Dapr 和 Spring Boot 集成添加到您的项目中

如果您已经有一个 Spring Boot 应用程序(Spring Boot 3.x+),可以直接将以下依赖项添加到您的项目中:

	<dependency>
        <groupId>io.dapr.spring</groupId>
		<artifactId>dapr-spring-boot-starter</artifactId>
		<version>0.13.1</version>
	</dependency>
	<dependency>
		<groupId>io.dapr.spring</groupId>
		<artifactId>dapr-spring-boot-starter-test</artifactId>
		<version>0.13.1</version>
		<scope>test</scope>
	</dependency>

通过添加这些依赖项,您可以:

  • 自动装配一个 DaprClient 以在您的应用程序中使用
  • 使用 Spring Data 和 Messaging 的抽象及编程模型,这些模型在底层使用 Dapr API
  • 通过依赖 Testcontainers 来引导 Dapr 控制平面服务和默认组件,从而改善您的开发流程

一旦这些依赖项在您的应用程序中,您可以依赖 Spring Boot 自动配置来自动装配一个 DaprClient 实例:

@Autowired
private DaprClient daprClient;

这将连接到默认的 Dapr gRPC 端点 localhost:50001,需要您在应用程序外启动 Dapr。

您可以在应用程序中的任何地方使用 DaprClient 与 Dapr API 交互,例如在 REST 端点内部:

@RestController
public class DemoRestController {
  @Autowired
  private DaprClient daprClient;

  @PostMapping("/store")
  public void storeOrder(@RequestBody Order order){
    daprClient.saveState("kvstore", order.orderId(), order).block();
  }
}

record Order(String orderId, Integer amount){}

如果您想避免在 Spring Boot 应用程序外管理 Dapr,可以依赖 Testcontainers 来在开发过程中引导 Dapr。为此,我们可以创建一个测试配置,使用 Testcontainers 来引导我们需要的所有内容,以使用 Dapr API 开发我们的应用程序。

通过使用 Testcontainers 和 Dapr 的集成,我们可以让 @TestConfiguration 为我们的应用程序引导 Dapr。注意,在此示例中,我们配置了一个名为 kvstore 的 Statestore 组件,该组件连接到由 Testcontainers 引导的 PostgreSQL 实例。

@TestConfiguration(proxyBeanMethods = false)
public class DaprTestContainersConfig {
  @Bean
  @ServiceConnection
  public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer){
    
    return new DaprContainer("daprio/daprd:1.14.1")
            .withAppName("producer-app")
            .withNetwork(daprNetwork)
            .withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
            .withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
            .dependsOn(postgreSQLContainer);
  }
}

在测试类路径中,您可以添加一个新的 Spring Boot 应用程序,使用此配置进行测试:

@SpringBootApplication
public class TestProducerApplication {

  public static void main(String[] args) {

    SpringApplication
            .from(ProducerApplication::main)
            .with(DaprTestContainersConfig.class)
            .run(args);
  }
  
}

现在您可以启动您的应用程序:

mvn spring-boot:test-run

运行此命令将启动应用程序,使用提供的测试配置,其中包括 Testcontainers 和 Dapr 集成。在日志中,您应该能够看到 daprdplacement 服务容器已为您的应用程序启动。

除了之前的配置(DaprTestContainersConfig),您的测试不应该测试 Dapr 本身,只需测试您的应用程序暴露的 REST 端点。

利用 Spring 和 Spring Boot 编程模型与 Dapr

Java SDK 允许您与所有 Dapr 构建块 接口。但如果您想利用 Spring 和 Spring Boot 编程模型,可以使用 dapr-spring-boot-starter 集成。这包括 Spring Data 的实现(KeyValueTemplateCrudRepository)以及用于生产和消费消息的 DaprMessagingTemplate(类似于 Spring KafkaSpring PulsarSpring AMQP for RabbitMQ)。

使用 Spring Data CrudRepositoryKeyValueTemplate

您可以使用依赖于 Dapr 实现的知名 Spring Data 构造。使用 Dapr,您无需添加任何与基础设施相关的驱动程序或客户端,使您的 Spring 应用程序更轻量化,并与其运行的环境解耦。

在底层,这些实现使用 Dapr Statestore 和 Binding API。

配置参数

使用 Spring Data 抽象,您可以配置 Dapr 将用于连接到可用基础设施的 statestore 和 bindings。这可以通过设置以下属性来完成:

dapr.statestore.name=kvstore
dapr.statestore.binding=kvbinding

然后您可以像这样 @Autowire 一个 KeyValueTemplateCrudRepository

@RestController
@EnableDaprRepositories
public class OrdersRestController {
  @Autowired
  private OrderRepository repository;
  
  @PostMapping("/orders")
  public void storeOrder(@RequestBody Order order){
    repository.save(order);
  }

  @GetMapping("/orders")
  public Iterable<Order> getAll(){
    return repository.findAll();
  }
}

其中 OrderRepository 在一个扩展 Spring Data CrudRepository 接口的接口中定义:

public interface OrderRepository extends CrudRepository<Order, String> {}

注意,@EnableDaprRepositories 注解完成了在 CrudRespository 接口下连接 Dapr API 的所有工作。因为 Dapr 允许用户从同一个应用程序与不同的 StateStores 交互,作为用户,您需要提供以下 bean 作为 Spring Boot @Configuration

@Configuration
@EnableConfigurationProperties({DaprStateStoreProperties.class})
public class ProducerAppConfiguration {
  
  @Bean
  public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper, DaprStateStoreProperties daprStatestoreProperties) {
    String storeName = daprStatestoreProperties.getName();
    String bindingName = daprStatestoreProperties.getBinding();

    return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName);
  }

  @Bean
  public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) {
    return new DaprKeyValueTemplate(keyValueAdapterResolver);
  }
}

使用 Spring Messaging 生产和消费事件

类似于 Spring Kafka、Spring Pulsar 和 Spring AMQP,您可以使用 DaprMessagingTemplate 将消息发布到配置的基础设施。要消费消息,您可以使用 @Topic 注解(即将重命名为 @DaprListener)。

要发布事件/消息,您可以在 Spring 应用程序中 @Autowired DaprMessagingTemplate。在此示例中,我们将发布 Order 事件,并将消息发送到名为 topic 的主题。

@Autowired
private DaprMessagingTemplate<Order> messagingTemplate;

@PostMapping("/orders")
public void storeOrder(@RequestBody Order order){
  repository.save(order);
  messagingTemplate.send("topic", order);
}

CrudRepository 类似,我们需要指定要使用哪个 PubSub 代理来发布和消费我们的消息。

dapr.pubsub.name=pubsub

因为使用 Dapr,您可以连接到多个 PubSub 代理,您需要提供以下 bean 以让 Dapr 知道您的 DaprMessagingTemplate 将使用哪个 PubSub 代理:

@Bean
public DaprMessagingTemplate<Order> messagingTemplate(DaprClient daprClient,
                                                             DaprPubSubProperties daprPubSubProperties) {
  return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName());
}

最后,因为 Dapr PubSub 需要您的应用程序和 Dapr 之间的双向连接,您需要使用一些参数扩展您的 Testcontainers 配置:

@Bean
@ServiceConnection
public DaprContainer daprContainer(Network daprNetwork, PostgreSQLContainer<?> postgreSQLContainer, RabbitMQContainer rabbitMQContainer){
    
    return new DaprContainer("daprio/daprd:1.14.1")
            .withAppName("producer-app")
            .withNetwork(daprNetwork)
            .withComponent(new Component("kvstore", "state.postgresql", "v1", STATE_STORE_PROPERTIES))
            .withComponent(new Component("kvbinding", "bindings.postgresql", "v1", BINDING_PROPERTIES))
            .withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties))
            .withAppPort(8080)
            .withAppChannelAddress("host.testcontainers.internal")
            .dependsOn(rabbitMQContainer)
            .dependsOn(postgreSQLContainer);
}

现在,在 Dapr 配置中,我们包含了一个 pubsub 组件,该组件将连接到由 Testcontainers 启动的 RabbitMQ 实例。我们还设置了两个重要参数 .withAppPort(8080).withAppChannelAddress("host.testcontainers.internal"),这允许 Dapr 在代理中发布消息时联系回应用程序。

要监听事件/消息,您需要在应用程序中暴露一个端点,该端点将负责接收消息。如果您暴露一个 REST 端点,可以使用 @Topic 注解让 Dapr 知道它需要将事件/消息转发到哪里:

@PostMapping("subscribe")
@Topic(pubsubName = "pubsub", name = "topic")
public void subscribe(@RequestBody CloudEvent<Order> cloudEvent){
    events.add(cloudEvent);
}

在引导您的应用程序时,Dapr 将注册订阅,以便将消息转发到您的应用程序暴露的 subscribe 端点。

如果您正在为这些订阅者编写测试,您需要确保 Testcontainers 知道您的应用程序将在端口 8080 上运行,以便 Testcontainers 启动的容器知道您的应用程序在哪里:

@BeforeAll
public static void setup(){
  org.testcontainers.Testcontainers.exposeHostPorts(8080);
}

您可以在此处查看并运行完整示例源代码

下一步

了解更多关于 Dapr Java SDK 可用于添加到您的 Java 应用程序的包的信息。

相关链接

4 - JavaScript SDK

用于开发Dapr应用的JavaScript SDK

这是一个用于在JavaScript和TypeScript中构建Dapr应用的开发库。该库对Dapr的常用API进行了抽象,如服务调用、状态管理、发布订阅、密钥管理等,并提供了一个简单直观的API接口来帮助构建应用。

安装

要开始使用JavaScript SDK,请从NPM安装Dapr JavaScript SDK:

npm install --save @dapr/dapr

结构

Dapr JavaScript SDK包含两个主要组件:

  • DaprServer:用于管理Dapr sidecar与应用之间的通信。
  • DaprClient:用于管理应用与Dapr sidecar之间的通信。

这些通信可以配置为使用gRPC或HTTP协议。

Dapr ServerDapr Client

入门

为了帮助您快速上手,请查看以下资源:

客户端

创建一个JavaScript客户端,与Dapr sidecar和其他Dapr应用进行交互(例如,发布事件,支持输出绑定等)。

服务器

创建一个JavaScript服务器,让Dapr sidecar与您的应用进行交互(例如,订阅事件,支持输入绑定等)。

虚拟演员

创建具有状态、提醒/计时器和方法的虚拟演员。


日志

配置和自定义SDK的日志功能。

示例

获取JavaScript SDK的源代码并尝试一些示例以快速入门。

4.1 - JavaScript 客户端 SDK

用于开发 Dapr 应用的 JavaScript 客户端 SDK

介绍

Dapr 客户端使您能够与 Dapr sidecar 进行通信,并访问其面向客户端的功能,如发布事件、调用输出绑定、状态管理、密钥管理等。

前置条件

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // 示例服务器的 Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 示例服务器的应用主机
const serverPort = "50051"; // 示例服务器的应用端口

// HTTP 示例
const client = new DaprClient({ daprHost, daprPort });

// GRPC 示例
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocolEnum.GRPC });

运行

要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。

使用 HTTP(默认)

import { DaprClient } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-http

使用 gRPC

由于 HTTP 是默认协议,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递一个额外的参数来实现这一点。

import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC });
# 使用 dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-grpc

环境变量

Dapr sidecar 端点

您可以使用 DAPR_HTTP_ENDPOINTDAPR_GRPC_ENDPOINT 环境变量分别设置 Dapr sidecar 的 HTTP 和 gRPC 端点。当这些变量被设置时,daprHostdaprPort 不需要在构造函数的选项参数中设置,客户端将自动从提供的端点中解析它们。

import { DaprClient, CommunicationProtocol } from "@dapr/dapr";

// 使用 HTTP,当 DAPR_HTTP_ENDPOINT 被设置时
const client = new DaprClient();

// 使用 gRPC,当 DAPR_GRPC_ENDPOINT 被设置时
const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC });

如果环境变量被设置,但 daprHostdaprPort 值被传递给构造函数,后者将优先于环境变量。

Dapr API 令牌

您可以使用 DAPR_API_TOKEN 环境变量设置 Dapr API 令牌。当此变量被设置时,daprApiToken 不需要在构造函数的选项参数中设置,客户端将自动获取它。

通用

增加主体大小

您可以通过使用 DaprClient 的选项增加应用程序与 sidecar 通信时使用的主体大小。

import { DaprClient, CommunicationProtocol } from "@dapr/dapr";

// 允许使用 10Mb 的主体大小
// 默认是 4Mb
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocol.HTTP,
  maxBodySizeMb: 10,
});

代理请求

通过代理请求,我们可以利用 Dapr 的 sidecar 架构带来的独特功能,如服务发现、日志记录等,使我们能够立即“升级”我们的 gRPC 服务。在 社区电话 41 中演示了 gRPC 代理的这一特性。

创建代理

要执行 gRPC 代理,只需通过调用 client.proxy.create() 方法创建一个代理:

// 一如既往,创建一个到我们 Dapr sidecar 的客户端
// 这个客户端负责确保 sidecar 已启动,我们可以通信,...
const clientSidecar = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocol.GRPC });

// 创建一个允许我们使用 gRPC 代码的代理
const clientProxy = await clientSidecar.proxy.create<GreeterClient>(GreeterClient);

我们现在可以调用在我们的 GreeterClient 接口中定义的方法(在这种情况下是来自 Hello World 示例

技术细节

架构

  1. gRPC 服务在 Dapr 中启动。我们通过 --app-port 告诉 Dapr 这个 gRPC 服务器运行在哪个端口,并通过 --app-id <APP_ID_HERE> 给它一个唯一的 Dapr 应用 ID
  2. 我们现在可以通过一个将连接到 sidecar 的客户端调用 Dapr sidecar
  3. 在调用 Dapr sidecar 时,我们提供一个名为 dapr-app-id 的元数据键,其值为在 Dapr 中启动的 gRPC 服务器(例如,在我们的示例中为 server
  4. Dapr 现在将调用转发到配置的 gRPC 服务器

构建块

JavaScript 客户端 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是客户端到 sidecar 的功能。

调用 API

调用服务

import { DaprClient, HttpMethod } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const serviceAppId = "my-app-id";
  const serviceMethod = "say-hello";

  // POST 请求
  const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.POST, { hello: "world" });

  // 带有头部的 POST 请求
  const response = await client.invoker.invoke(
    serviceAppId,
    serviceMethod,
    HttpMethod.POST,
    { hello: "world" },
    { headers: { "X-User-ID": "123" } },
  );

  // GET 请求
  const response = await client.invoker.invoke(serviceAppId, serviceMethod, HttpMethod.GET);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关服务调用的完整指南,请访问 如何:调用服务

状态管理 API

保存、获取和删除应用状态

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const serviceStoreName = "my-state-store-name";

  // 保存状态
  const response = await client.state.save(
    serviceStoreName,
    [
      {
        key: "first-key-name",
        value: "hello",
        metadata: {
          foo: "bar",
        },
      },
      {
        key: "second-key-name",
        value: "world",
      },
    ],
    {
      metadata: {
        ttlInSeconds: "3", // 这应该覆盖状态项中的 ttl
      },
    },
  );

  // 获取状态
  const response = await client.state.get(serviceStoreName, "first-key-name");

  // 获取批量状态
  const response = await client.state.getBulk(serviceStoreName, ["first-key-name", "second-key-name"]);

  // 状态事务
  await client.state.transaction(serviceStoreName, [
    {
      operation: "upsert",
      request: {
        key: "first-key-name",
        value: "new-data",
      },
    },
    {
      operation: "delete",
      request: {
        key: "second-key-name",
      },
    },
  ]);

  // 删除状态
  const response = await client.state.delete(serviceStoreName, "first-key-name");
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关状态操作的完整列表,请访问 如何:获取和保存状态

查询状态 API

import { DaprClient } from "@dapr/dapr";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const res = await client.state.query("state-mongodb", {
    filter: {
      OR: [
        {
          EQ: { "person.org": "Dev Ops" },
        },
        {
          AND: [
            {
              EQ: { "person.org": "Finance" },
            },
            {
              IN: { state: ["CA", "WA"] },
            },
          ],
        },
      ],
    },
    sort: [
      {
        key: "state",
        order: "DESC",
      },
    ],
    page: {
      limit: 10,
    },
  });

  console.log(res);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

PubSub API

发布消息

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 以 text/plain 格式发布消息到主题
  // 注意,内容类型是从消息类型推断的,除非明确指定
  const response = await client.pubsub.publish(pubSubName, topic, "hello, world!");
  // 如果发布失败,响应包含错误
  console.log(response);

  // 以 application/json 格式发布消息到主题
  await client.pubsub.publish(pubSubName, topic, { hello: "world" });

  // 将 JSON 消息作为纯文本发布
  const options = { contentType: "text/plain" };
  await client.pubsub.publish(pubSubName, topic, { hello: "world" }, options);

  // 以 application/cloudevents+json 格式发布消息到主题
  // 您还可以使用 cloudevent SDK 创建云事件 https://github.com/cloudevents/sdk-javascript
  const cloudEvent = {
    specversion: "1.0",
    source: "/some/source",
    type: "example",
    id: "1234",
  };
  await client.pubsub.publish(pubSubName, topic, cloudEvent);

  // 将 cloudevent 作为原始负载发布
  const options = { metadata: { rawPayload: true } };
  await client.pubsub.publish(pubSubName, topic, "hello, world!", options);

  // 以 text/plain 格式批量发布多个消息到主题
  await client.pubsub.publishBulk(pubSubName, topic, ["message 1", "message 2", "message 3"]);

  // 以 application/json 格式批量发布多个消息到主题
  await client.pubsub.publishBulk(pubSubName, topic, [
    { hello: "message 1" },
    { hello: "message 2" },
    { hello: "message 3" },
  ]);

  // 使用显式批量发布消息批量发布多个消息
  const bulkPublishMessages = [
    {
      entryID: "entry-1",
      contentType: "application/json",
      event: { hello: "foo message 1" },
    },
    {
      entryID: "entry-2",
      contentType: "application/cloudevents+json",
      event: { ...cloudEvent, data: "foo message 2", datacontenttype: "text/plain" },
    },
    {
      entryID: "entry-3",
      contentType: "text/plain",
      event: "foo message 3",
    },
  ];
  await client.pubsub.publishBulk(pubSubName, topic, bulkPublishMessages);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Bindings API

调用输出绑定

输出绑定

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const bindingName = "my-binding-name";
  const bindingOperation = "create";
  const message = { hello: "world" };

  const response = await client.binding.send(bindingName, bindingOperation, message);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关输出绑定的完整指南,请访问 如何:使用绑定

Secret API

检索 secrets

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const secretStoreName = "my-secret-store";
  const secretKey = "secret-key";

  // 从 secret 存储中检索单个 secret
  const response = await client.secret.get(secretStoreName, secretKey);

  // 从 secret 存储中检索所有 secrets
  const response = await client.secret.getBulk(secretStoreName);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关 secrets 的完整指南,请访问 如何:检索 secrets

Configuration API

获取配置键

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort: process.env.DAPR_GRPC_PORT,
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  const config = await client.configuration.get("config-store", ["key1", "key2"]);
  console.log(config);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

示例输出:

{
   items: {
     key1: { key: 'key1', value: 'foo', version: '', metadata: {} },
     key2: { key: 'key2', value: 'bar2', version: '', metadata: {} }
   }
}

订阅配置更新

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort: process.env.DAPR_GRPC_PORT,
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  // 订阅配置存储更改的键 "key1" 和 "key2"
  const stream = await client.configuration.subscribeWithKeys("config-store", ["key1", "key2"], async (data) => {
    console.log("订阅接收到来自配置存储的更新:", data);
  });

  // 等待 60 秒并取消订阅。
  await new Promise((resolve) => setTimeout(resolve, 60000));
  stream.stop();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

示例输出:

订阅接收到来自配置存储的更新: {
  items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } }
}
订阅接收到来自配置存储的更新: {
  items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } }
}

Cryptography API

JavaScript SDK 中的 gRPC 客户端仅支持 cryptography API。

import { createReadStream, createWriteStream } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { pipeline } from "node:stream/promises";

import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "50050"; // 示例服务器的 Dapr sidecar 端口

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort,
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  // 使用流加密和解密消息
  await encryptDecryptStream(client);

  // 从缓冲区加密和解密消息
  await encryptDecryptBuffer(client);
}

async function encryptDecryptStream(client: DaprClient) {
  // 首先,加密消息
  console.log("== 使用流加密消息");
  console.log("将 plaintext.txt 加密为 ciphertext.out");

  await pipeline(
    createReadStream("plaintext.txt"),
    await client.crypto.encrypt({
      componentName: "crypto-local",
      keyName: "symmetric256",
      keyWrapAlgorithm: "A256KW",
    }),
    createWriteStream("ciphertext.out"),
  );

  // 解密消息
  console.log("== 使用流解密消息");
  console.log("将 ciphertext.out 解密为 plaintext.out");
  await pipeline(
    createReadStream("ciphertext.out"),
    await client.crypto.decrypt({
      componentName: "crypto-local",
    }),
    createWriteStream("plaintext.out"),
  );
}

async function encryptDecryptBuffer(client: DaprClient) {
  // 读取 "plaintext.txt" 以便我们有一些内容
  const plaintext = await readFile("plaintext.txt");

  // 首先,加密消息
  console.log("== 使用缓冲区加密消息");

  const ciphertext = await client.crypto.encrypt(plaintext, {
    componentName: "crypto-local",
    keyName: "my-rsa-key",
    keyWrapAlgorithm: "RSA",
  });

  await writeFile("test.out", ciphertext);

  // 解密消息
  console.log("== 使用缓冲区解密消息");
  const decrypted = await client.crypto.decrypt(ciphertext, {
    componentName: "crypto-local",
  });

  // 内容应该相等
  if (plaintext.compare(decrypted) !== 0) {
    throw new Error("解密的消息与原始消息不匹配");
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关 cryptography 的完整指南,请访问 如何:Cryptography

分布式锁 API

尝试锁定和解锁 API

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse";

const daprHost = "127.0.0.1";
const daprPortDefault = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const storeName = "redislock";
  const resourceId = "resourceId";
  const lockOwner = "owner1";
  let expiryInSeconds = 1000;

  console.log(`在 ${storeName}, ${resourceId} 上以所有者:${lockOwner} 获取锁`);
  const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
  console.log(lockResponse);

  console.log(`在 ${storeName}, ${resourceId} 上以所有者:${lockOwner} 解锁`);
  const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);
  console.log("解锁 API 响应:" + getResponseStatus(unlockResponse.status));
}

function getResponseStatus(status: LockStatus) {
  switch (status) {
    case LockStatus.Success:
      return "成功";
    case LockStatus.LockDoesNotExist:
      return "锁不存在";
    case LockStatus.LockBelongsToOthers:
      return "锁属于他人";
    default:
      return "内部错误";
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关分布式锁的完整指南,请访问 如何:使用分布式锁

Workflow API

Workflow 管理

import { DaprClient } from "@dapr/dapr";

async function start() {
  const client = new DaprClient();

  // 启动一个新的 workflow 实例
  const instanceId = await client.workflow.start("OrderProcessingWorkflow", {
    Name: "Paperclips",
    TotalCost: 99.95,
    Quantity: 4,
  });
  console.log(`启动了 workflow 实例 ${instanceId}`);

  // 获取一个 workflow 实例
  const workflow = await client.workflow.get(instanceId);
  console.log(
    `Workflow ${workflow.workflowName}, 创建于 ${workflow.createdAt.toUTCString()}, 状态为 ${
      workflow.runtimeStatus
    }`,
  );
  console.log(`附加属性:${JSON.stringify(workflow.properties)}`);

  // 暂停一个 workflow 实例
  await client.workflow.pause(instanceId);
  console.log(`暂停了 workflow 实例 ${instanceId}`);

  // 恢复一个 workflow 实例
  await client.workflow.resume(instanceId);
  console.log(`恢复了 workflow 实例 ${instanceId}`);

  // 终止一个 workflow 实例
  await client.workflow.terminate(instanceId);
  console.log(`终止了 workflow 实例 ${instanceId}`);

  // 清除一个 workflow 实例
  await client.workflow.purge(instanceId);
  console.log(`清除了 workflow 实例 ${instanceId}`);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

相关链接

4.2 - JavaScript 服务器 SDK

用于开发 Dapr 应用的 JavaScript 服务器 SDK

介绍

Dapr 服务器使您能够接收来自 Dapr sidecar 的通信,并访问其面向服务器的功能,例如:事件订阅、接收输入绑定等。

准备条件

安装和导入 Dapr 的 JS SDK

  1. 使用 npm 安装 SDK:
npm i @dapr/dapr --save
  1. 导入库:
import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

// HTTP 示例
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP, // DaprClient 使用与 DaprServer 相同的通信协议,除非另有说明
  clientOptions: {
    daprHost,
    daprPort,
  },
});

// GRPC 示例
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost,
    daprPort,
  },
});

运行

要运行示例,您可以使用两种不同的协议与 Dapr sidecar 交互:HTTP(默认)或 gRPC。

使用 HTTP(内置 express 网络服务器)

import { DaprServer } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: appHost,
  serverPort: appPort,
  clientOptions: {
    daprHost,
    daprPort,
  },
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server.start();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-http

ℹ️ 注意: 这里需要 app-port,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。

使用 HTTP(自带 express 网络服务器)

除了使用内置的网络服务器进行 Dapr sidecar 到应用程序的通信,您还可以自带实例。这在构建 REST API 后端并希望直接集成 Dapr 时非常有用。

注意,这目前仅适用于 express

💡 注意:使用自定义网络服务器时,SDK 将配置服务器属性,如最大主体大小,并向其添加新路由。这些路由是独特的,以避免与您的应用程序发生任何冲突,但不能保证不发生冲突。

import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";
import express from "express";

const myApp = express();

myApp.get("/my-custom-endpoint", (req, res) => {
  res.send({ msg: "My own express app!" });
});

const daprServer = new DaprServer({
      serverHost: "127.0.0.1", // 应用主机
      serverPort: "50002", // 应用端口
      serverHttp: myApp,
      clientOptions: {
        daprHost,
        daprPort
      }
    });

// 在服务器启动前初始化订阅,Dapr sidecar 使用它。
// 这也将初始化应用服务器本身(无需调用 `app.listen`)。
await daprServer.start();

配置完上述内容后,您可以像往常一样调用您的自定义端点:

const res = await fetch(`http://127.0.0.1:50002/my-custom-endpoint`);
const json = await res.json();

使用 gRPC

由于 HTTP 是默认的,您需要调整通信协议以使用 gRPC。您可以通过向客户端或服务器构造函数传递额外的参数来实现这一点。

import { DaprServer, CommunicationProtocol } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: appHost,
  serverPort: appPort,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost,
    daprPort,
  },
});
// 在服务器启动前初始化订阅,Dapr sidecar 依赖于这些
await server.start();
# 使用 dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start

# 或者,使用 npm 脚本
npm run start:dapr-grpc

ℹ️ 注意: 这里需要 app-port,因为这是我们的服务器需要绑定的地方。Dapr 将检查应用程序是否绑定到此端口,然后完成启动。

构建块

JavaScript 服务器 SDK 允许您与所有 Dapr 构建块 进行接口交互,重点是 sidecar 到应用程序的功能。

调用 API

监听调用

import { DaprServer, DaprInvokerCallbackContent } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const callbackFunction = (data: DaprInvokerCallbackContent) => {
    console.log("Received body: ", data.body);
    console.log("Received metadata: ", data.metadata);
    console.log("Received query: ", data.query);
    console.log("Received headers: ", data.headers); // 仅在 HTTP 中可用
  };

  await server.invoker.listen("hello-world", callbackFunction, { method: HttpMethod.GET });

  // 您现在可以使用您的应用 ID 和方法 "hello-world" 调用服务

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关服务调用的完整指南,请访问 如何:调用服务

PubSub API

订阅消息

可以通过多种方式订阅消息,以提供接收主题消息的灵活性:

  • 通过 subscribe 方法直接订阅
  • 通过 subscribeWithOptions 方法直接订阅并带有选项
  • 通过 susbcribeOnEvent 方法之后订阅

每次事件到达时,我们将其主体作为 data 传递,并将头信息作为 headers 传递,其中可以包含事件发布者的属性(例如,来自 IoT Hub 的设备 ID)

Dapr 要求在启动时设置订阅,但在 JS SDK 中,我们允许之后添加事件处理程序,为您提供编程的灵活性。

下面提供了一个示例

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 为主题配置订阅者
  // 方法 1:通过 `subscribe` 方法直接订阅
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
  );

  // 方法 2:通过 `subscribeWithOptions` 方法直接订阅并带有选项
  await server.pubsub.subscribeWithOptions(pubSubName, topic, {
    callback: async (data: any, headers: object) =>
      console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`),
  });

  // 方法 3:通过 `susbcribeOnEvent` 方法之后订阅
  // 注意:我们使用默认值,因为如果没有传递路由(空选项),我们将使用 "default" 作为路由名称
  await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", {});
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async (data: any, headers: object) => {
    console.log(`Received Data: ${JSON.stringify(data)} with headers: ${JSON.stringify(headers)}`);
  });

  // 启动服务器
  await server.start();
}

有关状态操作的完整列表,请访问 如何:发布和订阅

使用 SUCCESS/RETRY/DROP 状态订阅

Dapr 支持 重试逻辑的状态码,以指定消息处理后应执行的操作。

⚠️ JS SDK 允许在同一主题上有多个回调,我们处理状态优先级为 RETRY > DROP > SUCCESS,默认为 SUCCESS

⚠️ 确保在应用程序中 配置弹性 以处理 RETRY 消息

在 JS SDK 中,我们通过 DaprPubSubStatusEnum 枚举支持这些消息。为了确保 Dapr 将重试,我们还配置了一个弹性策略。

components/resiliency.yaml

apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
  name: myresiliency
spec:
  policies:
    retries:
      # 全局重试策略用于入站组件操作
      DefaultComponentInboundRetryPolicy:
        policy: constant
        duration: 500ms
        maxRetries: 10
  targets:
    components:
      messagebus:
        inbound:
          retry: DefaultComponentInboundRetryPolicy

src/index.ts

import { DaprServer, DaprPubSubStatusEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 成功处理消息
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
    return DaprPubSubStatusEnum.SUCCESS;
  });

  // 重试消息
  // 注意:此示例将继续重试传递消息
  // 注意 2:每个组件可以有自己的重试配置
  //   例如,https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-redis-pubsub/
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
    return DaprPubSubStatusEnum.RETRY;
  });

  // 丢弃消息
  await server.pubsub.subscribe(pubSubName, topic, async (data: any, headers: object) => {
    return DaprPubSubStatusEnum.DROP;
  });

  // 启动服务器
  await server.start();
}

基于规则订阅消息

Dapr 支持路由消息 到不同的处理程序(路由)基于规则。

例如,您正在编写一个需要根据消息的 “type” 处理消息的应用程序,使用 Dapr,您可以将它们发送到不同的路由 handlerType1handlerType2,默认路由为 handlerDefault

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";
  const topic = "topic-a";

  // 为主题配置订阅者并设置规则
  // 注意:默认路由和匹配模式是可选的
  await server.pubsub.subscribe("pubsub-redis", "topic-1", {
    default: "/default",
    rules: [
      {
        match: `event.type == "my-type-1"`,
        path: "/type-1",
      },
      {
        match: `event.type == "my-type-2"`,
        path: "/type-2",
      },
    ],
  });

  // 为每个路由添加处理程序
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "default", async (data) => {
    console.log(`Handling Default`);
  });
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-1", async (data) => {
    console.log(`Handling Type 1`);
  });
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-1", "type-2", async (data) => {
    console.log(`Handling Type 2`);
  });

  // 启动服务器
  await server.start();
}

使用通配符订阅

支持流行的通配符 *+(请确保验证 pubsub 组件是否支持)并可以按如下方式订阅:

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";

  // * 通配符
  await server.pubsub.subscribe(pubSubName, "/events/*", async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)}`),
  );

  // + 通配符
  await server.pubsub.subscribe(pubSubName, "/events/+/temperature", async (data: any, headers: object) =>
    console.log(`Received Data: ${JSON.stringify(data)}`),
  );

  // 启动服务器
  await server.start();
}

批量订阅消息

支持批量订阅,并可通过以下 API 获得:

  • 通过 subscribeBulk 方法进行批量订阅:maxMessagesCountmaxAwaitDurationMs 是可选的;如果未提供,将使用相关组件的默认值。

在监听消息时,应用程序以批量方式从 Dapr 接收消息。然而,与常规订阅一样,回调函数一次接收一条消息,用户可以选择返回 DaprPubSubStatusEnum 值以确认成功、重试或丢弃消息。默认行为是返回成功响应。

请参阅 此文档 以获取更多详细信息。

import { DaprServer } from "@dapr/dapr";

const pubSubName = "orderPubSub";
const topic = "topicbulk";

const daprHost = process.env.DAPR_HOST || "127.0.0.1";
const daprHttpPort = process.env.DAPR_HTTP_PORT || "3502";
const serverHost = process.env.SERVER_HOST || "127.0.0.1";
const serverPort = process.env.APP_PORT || 5001;

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort: daprHttpPort,
    },
  });

  // 使用默认配置向主题发布多条消息。
  await client.pubsub.subscribeBulk(pubSubName, topic, (data) =>
    console.log("Subscriber received: " + JSON.stringify(data)),
  );

  // 使用特定的 maxMessagesCount 和 maxAwaitDurationMs 向主题发布多条消息。
  await client.pubsub.subscribeBulk(
    pubSubName,
    topic,
    (data) => {
      console.log("Subscriber received: " + JSON.stringify(data));
      return DaprPubSubStatusEnum.SUCCESS; // 如果应用程序没有返回任何内容,默认是 SUCCESS。应用程序还可以根据传入的消息返回 RETRY 或 DROP。
    },
    {
      maxMessagesCount: 100,
      maxAwaitDurationMs: 40,
    },
  );
}

死信主题

Dapr 支持 死信主题。这意味着当消息处理失败时,它会被发送到死信队列。例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed。 例如,当消息在 /my-queue 上处理失败时,它将被发送到 /my-queue-failed

您可以使用 subscribeWithOptions 方法的以下选项:

  • deadletterTopic:指定死信主题名称(注意:如果未提供,我们将创建一个名为 deadletter 的主题)
  • deadletterCallback:作为死信处理程序触发的方法

在 JS SDK 中实现死信支持可以通过以下方式:

  • 作为选项传递 deadletterCallback
  • 通过 subscribeToRoute 手动订阅路由

下面提供了一个示例

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr sidecar 主机
const daprPort = "3500"; // Dapr sidecar 端口
const serverHost = "127.0.0.1"; // 应用主机
const serverPort = "50051"; // 应用端口

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const pubSubName = "my-pubsub-name";

  // 方法 1(通过 subscribeWithOptions 直接订阅)
  await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-5", {
    callback: async (data: any) => {
      throw new Error("Triggering Deadletter");
    },
    deadLetterCallback: async (data: any) => {
      console.log("Handling Deadletter message");
    },
  });

  // 方法 2(之后订阅)
  await server.pubsub.subscribeWithOptions("pubsub-redis", "topic-options-1", {
    deadletterTopic: "my-deadletter-topic",
  });
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "default", async () => {
    throw new Error("Triggering Deadletter");
  });
  server.pubsub.subscribeToRoute("pubsub-redis", "topic-options-1", "my-deadletter-topic", async () => {
    console.log("Handling Deadletter message");
  });

  // 启动服务器
  await server.start();
}

Bindings API

接收输入绑定

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const bindingName = "my-binding-name";

  const response = await server.binding.receive(bindingName, async (data: any) =>
    console.log(`Got Data: ${JSON.stringify(data)}`),
  );

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

有关输出绑定的完整指南,请访问 如何:使用绑定

Configuration API

💡 配置 API 目前仅通过 gRPC 可用

获取配置值

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort,
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });
  const config = await client.configuration.get("config-redis", ["myconfigkey1", "myconfigkey2"]);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

订阅键更改

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort,
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });
  const stream = await client.configuration.subscribeWithKeys("config-redis", ["myconfigkey1", "myconfigkey2"], () => {
    // 收到键更新
  });

  // 当您准备好停止监听时,调用以下命令
  await stream.close();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

相关链接

4.3 - JavaScript SDK for Actors

如何使用 Dapr JavaScript SDK 快速上手 actor

Dapr actors 包允许您通过 JavaScript 应用程序与 Dapr 虚拟 actor 交互。以下示例展示了如何使用 JavaScript SDK 与虚拟 actor 进行交互。

有关 Dapr actor 的详细介绍,请访问 actor 概述页面

前置条件

场景

以下代码示例大致描述了一个停车场车位监控系统的场景,可以在 Mark Russinovich 的这个视频中看到。

一个停车场由数百个停车位组成,每个停车位都配有一个传感器,该传感器向集中监控系统提供更新。停车位传感器(即我们的 actor)用于检测停车位是否被占用或可用。

要运行此示例,请克隆源代码,源代码位于 JavaScript SDK 示例目录中。

Actor 接口

actor 接口定义了 actor 实现和调用 actor 的客户端之间共享的契约。在下面的示例中,我们为停车场传感器创建了一个接口。每个传感器都有两个方法:carEntercarLeave,它们定义了停车位的状态:

export default interface ParkingSensorInterface {
  carEnter(): Promise<void>;
  carLeave(): Promise<void>;
}

Actor 实现

actor 实现通过扩展基类型 AbstractActor 并实现 actor 接口(在此示例中为 ParkingSensorInterface)来定义一个类。

以下代码描述了一个 actor 实现以及一些辅助方法。

import { AbstractActor } from "@dapr/dapr";
import ParkingSensorInterface from "./ParkingSensorInterface";

export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  async carEnter(): Promise<void> {
    // 实现更新停车位被占用的状态。
  }

  async carLeave(): Promise<void> {
    // 实现更新停车位可用的状态。
  }

  private async getInfo(): Promise<object> {
    // 实现从停车位传感器请求更新。
  }

  /**
   * @override
   */
  async onActivate(): Promise<void> {
    // 由 AbstractActor 调用的初始化逻辑。
  }
}

配置 Actor 运行时

要配置 actor 运行时,请使用 DaprClientOptions。各种参数及其默认值记录在 如何:在 Dapr 中使用虚拟 actor中。

注意,超时和间隔应格式化为 time.ParseDuration 字符串,这是一种用于表示时间段的格式。

import { CommunicationProtocolEnum, DaprClient, DaprServer } from "@dapr/dapr";

// 使用 DaprClientOptions 配置 actor 运行时。
const clientOptions = {
  daprHost: daprHost,
  daprPort: daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  actor: {
    actorIdleTimeout: "1h",
    actorScanInterval: "30s",
    drainOngoingCallTimeout: "1m",
    drainRebalancedActors: true,
    reentrancy: {
      enabled: true,
      maxStackDepth: 32,
    },
    remindersStoragePartitions: 0,
  },
};

// 在创建 DaprServer 和 DaprClient 时使用这些选项。

// 注意,DaprServer 内部创建了一个 DaprClient,需要使用 clientOptions 进行配置。
const server = new DaprServer({ serverHost, serverPort, clientOptions });

const client = new DaprClient(clientOptions);

注册 Actor

使用 DaprServer 包初始化并注册您的 actor:

import { DaprServer } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";

const daprHost = "127.0.0.1";
const daprPort = "50000";
const serverHost = "127.0.0.1";
const serverPort = "50001";

const server = new DaprServer({
  serverHost,
  serverPort,
  clientOptions: {
    daprHost,
    daprPort,
  },
});

await server.actor.init(); // 让服务器知道我们需要 actor
server.actor.registerActor(ParkingSensorImpl); // 注册 actor
await server.start(); // 启动服务器

// 要获取已注册的 actor,可以调用 `getRegisteredActors`:
const resRegisteredActors = await server.actor.getRegisteredActors();
console.log(`Registered Actors: ${JSON.stringify(resRegisteredActors)}`);

调用 Actor 方法

在注册 actor 之后,使用 ActorProxyBuilder 创建一个实现 ParkingSensorInterface 的代理对象。您可以通过直接调用代理对象上的方法来调用 actor 方法。在内部,它会转换为对 actor API 的网络调用并获取结果。

import { ActorId, DaprClient } from "@dapr/dapr";
import ParkingSensorImpl from "./ParkingSensorImpl";
import ParkingSensorInterface from "./ParkingSensorInterface";

const daprHost = "127.0.0.1";
const daprPort = "50000";

const client = new DaprClient({ daprHost, daprPort });

// 创建一个新的 actor 构建器。它可以用于创建多种类型的 actor。
const builder = new ActorProxyBuilder<ParkingSensorInterface>(ParkingSensorImpl, client);

// 创建一个新的 actor 实例。
const actor = builder.build(new ActorId("my-actor"));
// 或者,使用随机 ID
// const actor = builder.build(ActorId.createRandomId());

// 调用方法。
await actor.carEnter();

使用 actor 的状态

import { AbstractActor } from "@dapr/dapr";
import ActorStateInterface from "./ActorStateInterface";

export default class ActorStateExample extends AbstractActor implements ActorStateInterface {
  async setState(key: string, value: any): Promise<void> {
    await this.getStateManager().setState(key, value);
    await this.getStateManager().saveState();
  }

  async removeState(key: string): Promise<void> {
    await this.getStateManager().removeState(key);
    await this.getStateManager().saveState();
  }

  // 使用特定类型获取状态
  async getState<T>(key: string): Promise<T | null> {
    return await this.getStateManager<T>().getState(key);
  }

  // 不指定类型获取状态为 `any`
  async getState(key: string): Promise<any> {
    return await this.getStateManager().getState(key);
  }
}

Actor 定时器和提醒

JS SDK 支持 actor 通过注册定时器或提醒来在自身上安排周期性工作。定时器和提醒之间的主要区别在于,Dapr actor 运行时在停用后不保留有关定时器的任何信息,但使用 Dapr actor 状态提供程序持久化提醒信息。

这种区别允许用户在轻量级但无状态的定时器与更耗资源但有状态的提醒之间进行权衡。

定时器和提醒的调度接口是相同的。有关调度配置的更深入了解,请参阅 actor 定时器和提醒文档

Actor 定时器

// ...

const actor = builder.build(new ActorId("my-actor"));

// 注册一个定时器
await actor.registerActorTimer(
  "timer-id", // 定时器的唯一名称。
  "cb-method", // 定时器触发时要执行的回调方法。
  Temporal.Duration.from({ seconds: 2 }), // DueTime
  Temporal.Duration.from({ seconds: 1 }), // Period
  Temporal.Duration.from({ seconds: 1 }), // TTL
  50, // 要发送到定时器回调的状态。
);

// 删除定时器
await actor.unregisterActorTimer("timer-id");

Actor 提醒

// ...

const actor = builder.build(new ActorId("my-actor"));

// 注册一个提醒,它有一个默认回调:`receiveReminder`
await actor.registerActorReminder(
  "reminder-id", // 提醒的唯一名称。
  Temporal.Duration.from({ seconds: 2 }), // DueTime
  Temporal.Duration.from({ seconds: 1 }), // Period
  Temporal.Duration.from({ seconds: 1 }), // TTL
  100, // 要发送到提醒回调的状态。
);

// 删除提醒
await actor.unregisterActorReminder("reminder-id");

要处理回调,您需要在 actor 中重写默认的 receiveReminder 实现。例如,从我们原来的 actor 实现中:

export default class ParkingSensorImpl extends AbstractActor implements ParkingSensorInterface {
  // ...

  /**
   * @override
   */
  async receiveReminder(state: any): Promise<void> {
    // 在这里处理
  }

  // ...
}

有关 actor 的完整指南,请访问 如何:在 Dapr 中使用虚拟 actor

4.4 - JavaScript SDK中的日志记录

配置JavaScript SDK中的日志记录

介绍

JavaScript SDK自带一个内置的Console日志记录器。SDK会生成各种内部日志,帮助用户理解事件流程并排查问题。用户可以自定义日志的详细程度,并提供自己的日志记录器实现。

配置日志级别

日志记录有五个级别,按重要性从高到低排列 - errorwarninfoverbosedebug。设置日志级别意味着日志记录器将记录所有该级别及更高重要性的日志。例如,设置为verbose级别意味着SDK不会记录debug级别的日志。默认的日志级别是info

Dapr Client

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";

// 创建一个日志级别设置为verbose的客户端实例。
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  logger: { level: LogLevel.Verbose },
});

有关如何使用Client的更多详细信息,请参见JavaScript Client

DaprServer

import { CommunicationProtocolEnum, DaprServer, LogLevel } from "@dapr/dapr";

// 创建一个日志级别设置为error的服务器实例。
const server = new DaprServer({
  serverHost,
  serverPort,
  clientOptions: {
    daprHost,
    daprPort,
    logger: { level: LogLevel.Error },
  },
});

有关如何使用Server的更多详细信息,请参见JavaScript Server

自定义LoggerService

JavaScript SDK使用内置的Console进行日志记录。要使用自定义日志记录器,如Winston或Pino,可以实现LoggerService接口。

基于Winston的日志记录:

创建LoggerService的新实现。

import { LoggerService } from "@dapr/dapr";
import * as winston from "winston";

export class WinstonLoggerService implements LoggerService {
  private logger;

  constructor() {
    this.logger = winston.createLogger({
      transports: [new winston.transports.Console(), new winston.transports.File({ filename: "combined.log" })],
    });
  }

  error(message: any, ...optionalParams: any[]): void {
    this.logger.error(message, ...optionalParams);
  }
  warn(message: any, ...optionalParams: any[]): void {
    this.logger.warn(message, ...optionalParams);
  }
  info(message: any, ...optionalParams: any[]): void {
    this.logger.info(message, ...optionalParams);
  }
  verbose(message: any, ...optionalParams: any[]): void {
    this.logger.verbose(message, ...optionalParams);
  }
  debug(message: any, ...optionalParams: any[]): void {
    this.logger.debug(message, ...optionalParams);
  }
}

将新的实现传递给SDK。

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";
import { WinstonLoggerService } from "./WinstonLoggerService";

const winstonLoggerService = new WinstonLoggerService();

// 创建一个日志级别设置为verbose且日志服务为winston的客户端实例。
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  logger: { level: LogLevel.Verbose, service: winstonLoggerService },
});

4.5 - JavaScript 示例

通过一些示例来学习如何使用 Dapr JavaScript SDK!

快速开始

相关文章

想要分享您的文章?告诉我们! 我们会将其添加到下面的列表中。

4.6 - 如何:在 JavaScript SDK 中编写和管理 Dapr 工作流

如何使用 Dapr JavaScript SDK 快速启动和运行工作流

我们将创建一个 Dapr 工作流并通过控制台调用它。在这个示例中,您将:

此示例在自托管模式下运行,使用 dapr init 的默认配置。

先决条件

设置环境

克隆 JavaScript SDK 仓库并进入其中。

git clone https://github.com/dapr/js-sdk
cd js-sdk

从 JavaScript SDK 根目录,导航到 Dapr 工作流示例。

cd examples/workflow/authoring

运行以下命令以安装运行此工作流示例所需的 Dapr JavaScript SDK 依赖。

npm install

运行 activity-sequence.ts

activity-sequence 文件在 Dapr 工作流运行时中注册了一个工作流和一个活动。工作流是按顺序执行的一系列活动。我们使用 DaprWorkflowClient 来调度一个新的工作流实例并等待其完成。

const daprHost = "localhost";
const daprPort = "50001";
const workflowClient = new DaprWorkflowClient({
  daprHost,
  daprPort,
});
const workflowRuntime = new WorkflowRuntime({
  daprHost,
  daprPort,
});

const hello = async (_: WorkflowActivityContext, name: string) => {
  return `Hello ${name}!`;
};

const sequence: TWorkflow = async function* (ctx: WorkflowContext): any {
  const cities: string[] = [];

  const result1 = yield ctx.callActivity(hello, "Tokyo");
  cities.push(result1);
  const result2 = yield ctx.callActivity(hello, "Seattle");
  cities.push(result2);
  const result3 = yield ctx.callActivity(hello, "London");
  cities.push(result3);

  return cities;
};

workflowRuntime.registerWorkflow(sequence).registerActivity(hello);

// 将 worker 启动包装在 try-catch 块中以处理启动期间的任何错误
try {
  await workflowRuntime.start();
  console.log("工作流运行时启动成功");
} catch (error) {
  console.error("启动工作流运行时出错:", error);
}

// 调度一个新的编排
try {
  const id = await workflowClient.scheduleNewWorkflow(sequence);
  console.log(`编排已调度,ID:${id}`);

  // 等待编排完成
  const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);

  console.log(`编排完成!结果:${state?.serializedOutput}`);
} catch (error) {
  console.error("调度或等待编排时出错:", error);
}

在上面的代码中:

  • workflowRuntime.registerWorkflow(sequence)sequence 注册为 Dapr 工作流运行时中的一个工作流。
  • await workflowRuntime.start(); 构建并启动 Dapr 工作流运行时中的引擎。
  • await workflowClient.scheduleNewWorkflow(sequence) 在 Dapr 工作流运行时中调度一个新的工作流实例。
  • await workflowClient.waitForWorkflowCompletion(id, undefined, 30) 等待工作流实例完成。

在终端中,执行以下命令以启动 activity-sequence.ts

npm run start:dapr:activity-sequence

预期输出

你已启动并运行!Dapr 和您的应用程序日志将出现在这里。

...

== APP == 编排已调度,ID:dc040bea-6436-4051-9166-c9294f9d2201
== APP == 等待 30 秒以完成实例 dc040bea-6436-4051-9166-c9294f9d2201...
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 0 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Tokyo!" (14 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 3 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello Seattle!" (16 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 6 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 等待 1 个任务和 0 个事件完成...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
== APP == 收到 "Activity Request" 工作项
== APP == 活动 hello 完成,输出 "Hello London!" (15 个字符)
== APP == 收到实例 id 为 'dc040bea-6436-4051-9166-c9294f9d2201' 的 "Orchestrator Request" 工作项
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 使用 9 个历史事件重建本地状态...
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 处理 2 个新历史事件:[ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 编排完成,状态为 COMPLETED
== APP == dc040bea-6436-4051-9166-c9294f9d2201: 返回 1 个动作
INFO[0006] dc040bea-6436-4051-9166-c9294f9d2201: 'sequence' 完成,状态为 COMPLETED。 app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.3
== APP == 实例 dc040bea-6436-4051-9166-c9294f9d2201 完成
== APP == 编排完成!结果:["Hello Tokyo!","Hello Seattle!","Hello London!"]

下一步

5 - Dapr PHP SDK

用于开发Dapr应用的PHP SDK包

Dapr提供了一个SDK,帮助开发PHP应用程序。通过它,您可以使用Dapr创建PHP客户端、服务器和虚拟actor。

设置

先决条件

可选先决条件

初始化您的项目

在您希望创建服务的目录中,运行composer init并回答提示的问题。 使用composer require dapr/php-sdk安装此SDK以及您可能需要的其他依赖项。

配置您的服务

创建一个config.php文件,并复制以下内容:

<?php

use Dapr\Actors\Generators\ProxyFactory;
use Dapr\Middleware\Defaults\{Response\ApplicationJson,Tracing};
use Psr\Log\LogLevel;
use function DI\{env,get};

return [
    // 设置日志级别
    'dapr.log.level'               => LogLevel::WARNING,

    // 在每个请求上生成一个新的代理 - 推荐用于开发
    'dapr.actors.proxy.generation' => ProxyFactory::GENERATED,
    
    // 在此处放置任何订阅
    'dapr.subscriptions'           => [],
    
    // 如果此服务将托管任何actor,请在此处添加它们
    'dapr.actors'                  => [],
    
    // 配置Dapr在多长时间后认为actor空闲
    'dapr.actors.idle_timeout'     => null,
    
    // 配置Dapr检查空闲actor的频率
    'dapr.actors.scan_interval'    => null,
    
    // 配置Dapr在关闭期间等待actor完成的时间
    'dapr.actors.drain_timeout'    => null,
    
    // 配置Dapr是否应等待actor完成
    'dapr.actors.drain_enabled'    => null,
    
    // 您可以在此处更改Dapr的端口设置
    'dapr.port'                    => env('DAPR_HTTP_PORT', '3500'),
    
    // 添加任何自定义序列化例程
    'dapr.serializers.custom'      => [],
    
    // 添加任何自定义反序列化例程
    'dapr.deserializers.custom'    => [],
    
    // 以下设置为默认中间件,按指定顺序处理
    'dapr.http.middleware.request'  => [get(Tracing::class)],
    'dapr.http.middleware.response' => [get(ApplicationJson::class), get(Tracing::class)],
];

创建您的服务

创建index.php并放入以下内容:

<?php

require_once __DIR__.'/vendor/autoload.php';

use Dapr\App;

$app = App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(__DIR__ . '/config.php'));
$app->get('/hello/{name}', function(string $name) {
    return ['hello' => $name];
});
$app->start();

试用

使用dapr init初始化Dapr,然后使用dapr run -a dev -p 3000 -- php -S 0.0.0.0:3000启动项目。

您现在可以打开一个网页浏览器并访问http://localhost:3000/hello/world,将world替换为您的名字、宠物的名字或您想要的任何内容。

恭喜,您已经创建了您的第一个Dapr服务!期待看到您会用它做些什么!

更多信息

5.1 - 虚拟Actor

如何构建actor

如果你对actor模式不熟悉,学习actor模式的最佳地方是Actor概述

在PHP SDK中,actor分为客户端和actor(也称为运行时)两部分。作为actor的客户端,你需要通过ActorProxy类与远程actor进行交互。此类通过几种配置策略之一动态生成代理类。

编写actor时,系统可以为你管理状态。你可以接入actor的生命周期,并定义提醒和定时器。这为你处理适合actor模式的各种问题提供了强大的能力。

Actor代理

每当你想与actor通信时,你需要获取一个代理对象来进行通信。代理负责序列化你的请求,反序列化响应,并将其返回给你,同时遵循指定接口定义的契约。

为了创建代理,你首先需要一个接口来定义如何与actor发送和接收内容。例如,如果你想与一个仅跟踪计数的计数actor通信,你可以定义如下接口:

<?php
#[\Dapr\Actors\Attributes\DaprType('Counter')]
interface ICount {
    function increment(int $amount = 1): void;
    function get_count(): int;
}

将此接口放在actor和客户端都可以访问的共享库中是个好主意(如果两者都是用PHP编写的)。DaprType属性告诉DaprClient要发送到的actor的名称。它应与实现的DaprType匹配,尽管你可以根据需要覆盖类型。

<?php
$app->run(function(\Dapr\Actors\ActorProxy $actorProxy) {
    $actor = $actorProxy->get(ICount::class, 'actor-id');
    $actor->increment(10);
});

编写Actor

要创建actor,你需要实现之前定义的接口,并添加DaprType属性。所有actor必须实现IActor,然而有一个Actor基类实现了样板代码,使你的实现更简单。

这是计数器actor:

<?php
#[\Dapr\Actors\Attributes\DaprType('Count')]
class Counter extends \Dapr\Actors\Actor implements ICount {
    function __construct(string $id, private CountState $state) {
        parent::__construct($id);
    }
    
    function increment(int $amount = 1): void {
        $this->state->count += $amount;
    }
    
    function get_count(): int {
        return $this->state->count;
    }
}

构造函数是最重要的部分。它至少需要一个名为id的参数,即actor的id。任何额外的参数都由DI容器注入,包括你想使用的任何ActorState

Actor生命周期

actor通过构造函数在每个针对该actor类型的请求中实例化。你可以使用它来计算临时状态或处理你需要的任何请求特定的启动,例如设置其他客户端或连接。

actor实例化后,可能会调用on_activation()方法。on_activation()方法在actor“唤醒”时或首次创建时调用。它不会在每个请求上调用。

接下来,调用actor方法。这可能来自定时器、提醒或客户端。你可以执行任何需要完成的工作和/或抛出异常。

最后,工作的结果返回给调用者。经过一段时间(取决于服务的配置方式),actor将被停用,并调用on_deactivation()方法。如果主机崩溃、daprd崩溃或发生其他错误导致无法成功调用,则可能不会调用此方法。

Actor State

actor状态是一个扩展ActorState的“普通旧PHP对象”(POPO)。ActorState基类提供了一些有用的方法。以下是一个示例实现:

<?php
class CountState extends \Dapr\Actors\ActorState {
    public int $count = 0;
}

注册Actor

Dapr期望在启动时知道服务可能托管的actor。你需要将其添加到配置中:

如果你想利用预编译的依赖注入,你需要使用工厂:

<?php
// 在config.php中

return [
    'dapr.actors' => fn() => [Counter::class],
];

启动应用所需的全部内容:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(
    configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php')->enableCompilation(__DIR__)
);
$app->start();
<?php
// 在config.php中

return [
    'dapr.actors' => [Counter::class]
];

启动应用所需的全部内容:

<?php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));
$app->start();

5.1.1 - 生产参考:actor

在生产环境中执行PHP角色

代理模式

actor代理有四种模式可供选择。每种模式都有不同的优缺点,您需要在开发和生产中进行权衡。

<?php
\Dapr\Actors\Generators\ProxyFactory::GENERATED;
\Dapr\Actors\Generators\ProxyFactory::GENERATED_CACHED;
\Dapr\Actors\Generators\ProxyFactory::ONLY_EXISTING;
\Dapr\Actors\Generators\ProxyFactory::DYNAMIC;

可以通过dapr.actors.proxy.generation配置键进行设置。

这是默认模式。在此模式下,每个请求都会生成一个类并通过eval执行。主要用于开发环境,不建议在生产中使用。

这与ProxyModes::GENERATED相同,但类会存储在一个临时文件中,因此不需要在每个请求时重新生成。由于无法判断何时更新缓存的类,因此不建议在开发中使用,但在无法手动生成文件时可以使用。

在此模式下,如果代理类不存在,则会抛出异常。这对于不希望在生产中生成代码的情况很有用。您必须确保类已生成并预加载/自动加载。

生成代理

您可以创建一个composer脚本以按需生成代理,以利用ONLY_EXISTING模式。

创建一个ProxyCompiler.php

<?php

class ProxyCompiler {
    private const PROXIES = [
        MyActorInterface::class,
        MyOtherActorInterface::class,
    ];
    
    private const PROXY_LOCATION = __DIR__.'/proxies/';
    
    public static function compile() {
        try {
            $app = \Dapr\App::create();
            foreach(self::PROXIES as $interface) {
                $output = $app->run(function(\DI\FactoryInterface $factory) use ($interface) {
                    return \Dapr\Actors\Generators\FileGenerator::generate($interface, $factory);
                });
                $reflection = new ReflectionClass($interface);
                $dapr_type = $reflection->getAttributes(\Dapr\Actors\Attributes\DaprType::class)[0]->newInstance()->type;
                $filename = 'dapr_proxy_'.$dapr_type.'.php';
                file_put_contents(self::PROXY_LOCATION.$filename, $output);
                echo "Compiled: $interface";
            }
        } catch (Exception $ex) {
            echo "Failed to generate proxy for $interface\n{$ex->getMessage()} on line {$ex->getLine()} in {$ex->getFile()}\n";
        }
    }
}

然后在composer.json中为生成的代理添加一个psr-4自动加载器和一个脚本:

{
  "autoload": {
    "psr-4": {
      "Dapr\\Proxies\\": "path/to/proxies"
    }
  },
  "scripts": {
    "compile-proxies": "ProxyCompiler::compile"
  }
}

最后,配置dapr仅使用生成的代理:

<?php
// 在config.php中

return [
    'dapr.actors.proxy.generation' => ProxyFactory::ONLY_EXISTING,
];

在此模式下,代理满足接口契约,但实际上并不实现接口本身(意味着instanceof将为false)。此模式利用PHP中的一些特性,适用于无法eval或生成代码的情况。

请求

创建actor代理在任何模式下都是非常高效的。在创建actor代理对象时没有请求。

当您调用代理对象上的方法时,只有您实现的方法由您的actor实现服务。get_id()在本地处理,而get_reminder()delete_reminder()等由daprd处理。

actor实现

每个PHP中的actor实现都必须实现\Dapr\Actors\IActor并使用\Dapr\Actors\ActorTrait特性。这允许快速反射和一些快捷方式。使用\Dapr\Actors\Actor抽象基类可以为您做到这一点,但如果您需要覆盖默认行为,可以通过实现接口和使用特性来实现。

激活和停用

当actor激活时,会将一个令牌文件写入临时目录(默认情况下在Linux中为'/tmp/dapr_' + sha256(concat(Dapr type, id)),在Windows中为'%temp%/dapr_' + sha256(concat(Dapr type, id)))。这会一直保留到actor停用或主机关闭。这允许在Dapr在主机上激活actor时仅调用一次on_activation

性能

在使用php-fpmnginx或Windows上的IIS的生产环境中,actor方法调用非常快。即使actor在每个请求中构建,actor状态键仅在需要时加载,而不是在每个请求中加载。然而,单独加载每个键会有一些开销。可以通过在状态中存储数据数组来缓解这一问题,以速度换取一些可用性。建议不要从一开始就这样做,而是在需要时作为优化。

状态版本控制

ActorState对象中的变量名称直接对应于存储中的键名。这意味着如果您更改变量的类型或名称,可能会遇到错误。为了解决这个问题,您可能需要对状态对象进行版本控制。为此,您需要覆盖状态的加载和存储方式。有很多方法可以解决这个问题,其中一种解决方案可能是这样的:

<?php

class VersionedState extends \Dapr\Actors\ActorState {
    /**
     * @var int 存储中状态的当前版本。我们给出当前版本的默认值。
     * 然而,它可能在存储中有不同的值。
     */
    public int $state_version = self::VERSION;
    
    /**
     * @var int 数据的当前版本
     */
    private const VERSION = 3;
    
    /**
     * 当您的actor激活时调用。
     */
    public function upgrade() {
        if($this->state_version < self::VERSION) {
            $value = parent::__get($this->get_versioned_key('key', $this->state_version));
            // 在更新数据结构后更新值
            parent::__set($this->get_versioned_key('key', self::VERSION), $value);
            $this->state_version = self::VERSION;
            $this->save_state();
        }
    }
    
    // 如果您在上面的方法中根据需要升级所有键,则在加载/保存时不需要遍历以前的键,
    // 您可以直接获取键的当前版本。
    
    private function get_previous_version(int $version): int {
        return $this->has_previous_version($version) ? $version - 1 : $version;
    }
    
    private function has_previous_version(int $version): bool {
        return $version >= 0;
    }
    
    private function walk_versions(int $version, callable $callback, callable $predicate): mixed {
        $value = $callback($version);
        if($predicate($value) || !$this->has_previous_version($version)) {
            return $value;
        }
        return $this->walk_versions($this->get_previous_version($version), $callback, $predicate);
    }
    
    private function get_versioned_key(string $key, int $version) {
        return $this->has_previous_version($version) ? $version.$key : $key;
    }
    
    public function __get(string $key): mixed {
        return $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__get($this->get_versioned_key($key, $version)),
            fn($value) => isset($value)
        );
    }
    
    public function __isset(string $key): bool {
        return $this->walk_versions(
            self::VERSION,
            fn($version) => parent::__isset($this->get_versioned_key($key, $version)),
            fn($isset) => $isset
        );
    }
    
    public function __set(string $key,mixed $value): void {
        // 可选:您可以取消设置键的以前版本
        parent::__set($this->get_versioned_key($key, self::VERSION), $value);
    }
    
    public function __unset(string $key) : void {
        // 取消设置此版本和所有以前版本
        $this->walk_versions(
            self::VERSION, 
            fn($version) => parent::__unset($this->get_versioned_key($key, $version)), 
            fn() => false
        );
    }
}

有很多可以优化的地方,在生产中直接使用这个不是一个好主意,但您可以了解它的工作原理。很多将取决于您的用例,这就是为什么在SDK中没有这样的东西。例如,在这个示例实现中,保留了以前的值,以防在升级期间可能出现错误;保留以前的值允许再次运行升级,但您可能希望删除以前的值。

5.2 - 使用 PHP 实现发布和订阅

如何使用

通过 Dapr,您可以发布各种类型的内容,包括云事件。SDK 提供了一个简单的云事件实现,您也可以传递符合云事件规范的数组或使用其他库。

<?php
$app->post('/publish', function(\Dapr\Client\DaprClient $daprClient) {
    $daprClient->publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: ['something' => 'happened']);
});

有关发布/订阅的更多信息,请查看操作指南

数据的内容类型

PHP SDK 允许您在构建自定义云事件或发布原始数据时设置数据的内容类型。

<?php
$event = new \Dapr\PubSub\CloudEvent();
$event->data = $xml;
$event->data_content_type = 'application/xml';
<?php
/**
 * @var \Dapr\Client\DaprClient $daprClient 
 */
$daprClient->publishEvent(pubsubName: 'pubsub', topicName: 'my-topic', data: $raw_data, contentType: 'application/octet-stream');

接收云事件

在您的订阅处理程序中,您可以让 DI 容器将 Dapr\PubSub\CloudEventarray 注入到您的控制器中。使用 Dapr\PubSub\CloudEvent 时,会进行一些验证以确保事件的正确性。如果您需要直接访问数据,或者事件不符合规范,请使用 array

5.3 - 应用程序

使用 App 类

PHP 中没有默认的路由器。因此,提供了 \Dapr\App 类。它底层使用了 Nikic 的 FastRoute。然而,你可以选择任何你喜欢的路由器或框架。只需查看 App 类中的 add_dapr_routes() 方法,了解 actor 和订阅是如何实现的。

每个应用程序都应该以 App::create() 开始,它接受两个参数,第一个是现有的 DI 容器(如果有的话),第二个是一个回调,用于挂钩到 ContainerBuilder 并添加你自己的配置。

接下来,你应该定义你的路由,然后调用 $app->start() 来执行当前请求的路由。

<?php
// app.php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));

// 添加一个控制器用于 GET /test/{id},返回 id
$app->get('/test/{id}', fn(string $id) => $id);

$app->start();

从控制器返回

你可以从控制器返回任何内容,它将被序列化为一个 JSON 对象。你也可以请求 Psr Response 对象并返回它,这样你就可以自定义头信息,并控制整个响应:

<?php
$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions('config.php'));

// 添加一个控制器用于 GET /test/{id},返回 id
$app->get('/test/{id}', 
    fn(
        string $id, 
        \Psr\Http\Message\ResponseInterface $response, 
        \Nyholm\Psr7\Factory\Psr17Factory $factory) => $response->withBody($factory->createStream($id)));

$app->start();

将应用程序用作客户端

当你只想将 Dapr 用作客户端时,比如在现有代码中,你可以调用 $app->run()。在这些情况下,通常不需要自定义配置,不过,在生产环境中你可能希望使用编译的 DI 容器:

<?php
// app.php

require_once __DIR__ . '/vendor/autoload.php';

$app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder) => $builder->enableCompilation(__DIR__));
$result = $app->run(fn(\Dapr\DaprClient $client) => $client->get('/invoke/other-app/method/my-method'));

在其他框架中使用

提供了一个 DaprClient 对象,实际上,App 对象使用的所有语法糖都是基于 DaprClient 构建的。

<?php

require_once __DIR__ . '/vendor/autoload.php';

$clientBuilder = \Dapr\Client\DaprClient::clientBuilder();

// 你可以自定义(反)序列化,或者注释掉以使用默认的 JSON 序列化器。
$clientBuilder = $clientBuilder->withSerializationConfig($yourSerializer)->withDeserializationConfig($yourDeserializer);

// 你也可以传递一个日志记录器
$clientBuilder = $clientBuilder->withLogger($myLogger);

// 并更改 sidecar 的 URL,例如,使用 https
$clientBuilder = $clientBuilder->useHttpClient('https://localhost:3800') 

在调用之前有几个函数可以使用

5.3.1 - 单元测试

单元测试

在 PHP SDK 中,单元测试和集成测试是非常重要的组成部分。通过使用依赖注入容器、模拟、存根以及提供的 \Dapr\Mocks\TestClient,可以实现非常精细的测试。

测试 Actor

在测试 Actor 时,我们主要关注两个方面:

  1. 基于初始状态的返回结果
  2. 基于初始状态的结果状态

以下是一个简单的 Actor 测试示例,该 Actor 会更新其状态并返回特定值:

<?php

// TestState.php

class TestState extends \Dapr\Actors\ActorState
{
    public int $number;
}

// TestActor.php

#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
    public function __construct(string $id, private TestState $state)
    {
        parent::__construct($id);
    }

    public function oddIncrement(): bool
    {
        if ($this->state->number % 2 === 0) {
            return false;
        }
        $this->state->number += 1;

        return true;
    }
}

// TheTest.php

class TheTest extends \PHPUnit\Framework\TestCase
{
    private \DI\Container $container;

    public function setUp(): void
    {
        parent::setUp();
        // 创建一个默认应用并从中获取 DI 容器
        $app = \Dapr\App::create(
            configure: fn(\DI\ContainerBuilder $builder) => $builder->addDefinitions(
            ['dapr.actors' => [TestActor::class]],
            [\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)]
        ));
        $app->run(fn(\DI\Container $container) => $this->container = $container);
    }

    public function testIncrementsWhenOdd()
    {
        $id      = uniqid();
        $runtime = $this->container->get(\Dapr\Actors\ActorRuntime::class);
        $client  = $this->getClient();

        // 模拟从 http://localhost:1313/reference/api/actors_api/ 获取当前状态
        $client->register_get("/actors/TestActor/$id/state/number", code: 200, data: 3);

        // 模拟从 http://localhost:1313/reference/api/actors_api/ 进行状态递增
        $client->register_post(
            "/actors/TestActor/$id/state",
            code: 204,
            response_data: null,
            expected_request: [
                [
                    'operation' => 'upsert',
                    'request'   => [
                        'key'   => 'number',
                        'value' => 4,
                    ],
                ],
            ]
        );

        $result = $runtime->resolve_actor(
            'TestActor',
            $id,
            fn($actor) => $runtime->do_method($actor, 'oddIncrement', null)
        );
        $this->assertTrue($result);
    }

    private function getClient(): \Dapr\Mocks\TestClient
    {
        return $this->container->get(\Dapr\DaprClient::class);
    }
}
<?php

// TestState.php

class TestState extends \Dapr\Actors\ActorState
{
    public int $number;
}

// TestActor.php

#[\Dapr\Actors\Attributes\DaprType('TestActor')]
class TestActor extends \Dapr\Actors\Actor
{
    public function __construct(string $id, private TestState $state)
    {
        parent::__construct($id);
    }

    public function oddIncrement(): bool
    {
        if ($this->state->number % 2 === 0) {
            return false;
        }
        $this->state->number += 1;

        return true;
    }
}

// TheTest.php

class TheTest extends \PHPUnit\Framework\TestCase
{
    public function testNotIncrementsWhenEven() {
        $container = new \DI\Container();
        $state = new TestState($container, $container);
        $state->number = 4;
        $id = uniqid();
        $actor = new TestActor($id, $state);
        $this->assertFalse($actor->oddIncrement());
        $this->assertSame(4, $state->number);
    }
}

测试事务

在构建事务时,您可能需要测试如何处理失败的事务。为此,您需要注入故障并确保事务按预期进行。

<?php

// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
    public string $value = '';
}

// SomeService.php
class SomeService {
    public function __construct(private MyState $state) {}

    public function doWork() {
        $this->state->begin();
        $this->state->value = "hello world";
        $this->state->commit();
    }
}

// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
    private \DI\Container $container;

    public function setUp(): void
    {
        parent::setUp();
        $app = \Dapr\App::create(configure: fn(\DI\ContainerBuilder $builder)
            => $builder->addDefinitions([\Dapr\DaprClient::class => \DI\autowire(\Dapr\Mocks\TestClient::class)]));
        $this->container = $app->run(fn(\DI\Container $container) => $container);
    }

    private function getClient(): \Dapr\Mocks\TestClient {
        return $this->container->get(\Dapr\DaprClient::class);
    }

    public function testTransactionFailure() {
        $client = $this->getClient();

        // 模拟从 https://v1-16.docs.dapr.io/zh-hans/reference/api/state_api/ 创建响应
        $client->register_post('/state/statestore/bulk', code: 200, response_data: [
            [
                'key' => 'value',
                // 没有先前的值
            ],
        ], expected_request: [
            'keys' => ['value'],
            'parallelism' => 10
        ]);
        $client->register_post('/state/statestore/transaction',
            code: 200,
            response_data: null,
            expected_request: [
                'operations' => [
                    [
                        'operation' => 'upsert',
                        'request' => [
                            'key' => 'value',
                            'value' => 'hello world'
                        ]
                    ]
                ]
            ]
        );
        $state = new MyState($this->container, $this->container);
        $service = new SomeService($state);
        $service->doWork();
        $this->assertSame('hello world', $state->value);
    }
}
<?php
// MyState.php
#[\Dapr\State\Attributes\StateStore('statestore', \Dapr\consistency\EventualFirstWrite::class)]
class MyState extends \Dapr\State\TransactionalState {
    public string $value = '';
}

// SomeService.php
class SomeService {
    public function __construct(private MyState $state) {}

    public function doWork() {
        $this->state->begin();
        $this->state->value = "hello world";
        $this->state->commit();
    }
}

// TheTest.php
class TheTest extends \PHPUnit\Framework\TestCase {
    public function testTransactionFailure() {
        $state = $this->createStub(MyState::class);
        $service = new SomeService($state);
        $service->doWork();
        $this->assertSame('hello world', $state->value);
    }
}

5.4 - 使用 PHP 进行状态管理

如何使用

Dapr 提供了一种模块化的状态管理方法,适用于您的应用程序。要学习基础知识,请访问 如何操作

元数据

许多状态组件允许您传递元数据给组件,以控制组件行为的特定方面。PHP SDK 允许您通过以下方式传递这些元数据:

<?php
// 使用状态管理器
$app->run(
    fn(\Dapr\State\StateManager $stateManager) => 
        $stateManager->save_state('statestore', new \Dapr\State\StateItem('key', 'value', metadata: ['port' => '112'])));

// 使用 DaprClient
$app->run(fn(\Dapr\Client\DaprClient $daprClient) => $daprClient->saveState(storeName: 'statestore', key: 'key', value: 'value', metadata: ['port' => '112']))

这是一个将端口元数据传递给 Cassandra 的示例。

每个状态操作都允许传递元数据。

一致性与并发性

在 PHP SDK 中,有四个类代表 Dapr 中的四种不同类型的一致性和并发性:

<?php
[
    \Dapr\consistency\StrongLastWrite::class, 
    \Dapr\consistency\StrongFirstWrite::class,
    \Dapr\consistency\EventualLastWrite::class,
    \Dapr\consistency\EventualFirstWrite::class,
] 

将其中一个传递给 StateManager 方法或使用 StateStore() 属性可以让您定义状态存储应如何处理冲突。

并行性

进行批量读取或开始事务时,您可以指定并行度。如果必须一次读取一个键,Dapr 将从底层存储中“最多”读取这么多键。这有助于在性能的代价下控制状态存储的负载。默认值是 10

前缀

硬编码的键名很有用,但让状态对象更具可重用性会更好。在提交事务或将对象保存到状态时,您可以传递一个前缀,该前缀应用于对象中的每个键。

<?php
class TransactionObject extends \Dapr\State\TransactionalState {
    public string $key;
}

$app->run(function (TransactionObject $object ) {
    $object->begin(prefix: 'my-prefix-');
    $object->key = 'value';
    // 提交到键 `my-prefix-key`
    $object->commit();
});
<?php
class StateObject {
    public string $key;
}

$app->run(function(\Dapr\State\StateManager $stateManager) {
    $stateManager->load_object($obj = new StateObject(), prefix: 'my-prefix-');
    // 原始值来自 `my-prefix-key`
    $obj->key = 'value';
    // 保存到 `my-prefix-key`
    $stateManager->save_object($obj, prefix: 'my-prefix-');
});

5.5 - 自定义序列化

如何配置序列化

Dapr 使用 JSON 进行序列化,因此在发送或接收数据时,复杂类型的信息可能会丢失。

序列化

当从控制器返回对象、将对象传递给 DaprClient 或将对象存储在状态存储中时,只有公共属性会被扫描和序列化。您可以通过实现 \Dapr\Serialization\ISerialize 接口来自定义此行为。例如,如果您想创建一个序列化为字符串的 ID 类型,可以这样实现:

<?php

class MyId implements \Dapr\Serialization\Serializers\ISerialize 
{
    public string $id;
    
    public function serialize(mixed $value, \Dapr\Serialization\ISerializer $serializer): mixed
    {
        // $value === $this
        return $this->id; 
    }
}

这种方法适用于我们完全控制的类型,但不适用于库或 PHP 自带的类。对于这些情况,您需要在依赖注入容器中注册一个自定义序列化器:

<?php
// 在 config.php 中

class SerializeSomeClass implements \Dapr\Serialization\Serializers\ISerialize 
{
    public function serialize(mixed $value, \Dapr\Serialization\ISerializer $serializer): mixed 
    {
        // 序列化 $value 并返回结果
    }
}

return [
    'dapr.serializers.custom' => [SomeClass::class => new SerializeSomeClass()],
];

反序列化

反序列化的过程与序列化类似,只是使用的接口是 \Dapr\Deserialization\Deserializers\IDeserialize

6 - Dapr Python SDK

用于开发Dapr应用的Python SDK包

Dapr 提供了多种子包以帮助开发 Python 应用程序。通过这些子包,您可以使用 Dapr 创建 Python 客户端、服务器和虚拟 actor。

先决条件

安装

要开始使用 Python SDK,请安装主要的 Dapr Python SDK 包。

pip install dapr

注意: 开发包包含与 Dapr 运行时预发布版本兼容的功能和行为。在安装 dapr-dev 包之前,请确保卸载任何稳定版本的 Python SDK。

pip install dapr-dev

可用子包

SDK 导入

Python SDK 导入是随主 SDK 安装一起包含的子包,但在使用时需要导入。Dapr Python SDK 提供的常用导入包括:

Client

编写 Python 应用以与 Dapr sidecar 和其他 Dapr 应用交互,包括 Python 中的有状态虚拟 actor。

Actors

创建和与 Dapr 的 actor 框架交互。

了解 所有可用的 Dapr Python SDK 导入 的更多信息。

SDK 扩展

SDK 扩展主要用于接收 pub/sub 事件、程序化创建 pub/sub 订阅和处理输入绑定事件。虽然这些任务可以在没有扩展的情况下完成,但使用 Python SDK 扩展会更加方便。

gRPC

使用 gRPC 服务器扩展创建 Dapr 服务。

FastAPI

使用 Dapr FastAPI 扩展与 Dapr Python 虚拟 actor 和 pub/sub 集成。

Flask

使用 Dapr Flask 扩展与 Dapr Python 虚拟 actor 集成。

Workflow

编写与其他 Dapr API 一起工作的 Python 工作流。

了解 Dapr Python SDK 扩展 的更多信息。

试用

克隆 Python SDK 仓库。

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

通过 Python 快速入门、教程和示例来体验 Dapr 的实际应用:

SDK 示例描述
快速入门使用 Python SDK 在几分钟内体验 Dapr 的 API 构建块。
SDK 示例克隆 SDK 仓库以尝试一些示例并开始。
绑定教程查看 Dapr Python SDK 如何与其他 Dapr SDK 一起工作以启用绑定。
分布式计算器教程使用 Dapr Python SDK 处理方法调用和状态持久化功能。
Hello World 教程学习如何在本地机器上使用 Python SDK 启动并运行 Dapr。
Hello Kubernetes 教程在 Kubernetes 集群中使用 Dapr Python SDK 启动并运行。
可观测性教程使用 Python SDK 探索 Dapr 的指标收集、跟踪、日志记录和健康检查功能。
Pub/sub 教程查看 Dapr Python SDK 如何与其他 Dapr SDK 一起工作以启用 pub/sub 应用。

更多信息

Serialization

了解有关 Dapr SDK 中的序列化的更多信息。

PyPI

Python 包索引

6.1 - 使用 Dapr 客户端 Python SDK 入门

如何使用 Dapr Python SDK 快速上手

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

准备工作

在开始之前,安装 Dapr Python 包

导入客户端包

dapr 包包含 DaprClient,用于创建和使用客户端。

from dapr.clients import DaprClient

初始化客户端

您可以通过多种方式初始化 Dapr 客户端:

默认值:

如果不提供参数初始化客户端,它将使用 Dapr sidecar 实例的默认值 (127.0.0.1:50001)。

from dapr.clients import DaprClient

with DaprClient() as d:
    # 使用客户端

在初始化时指定端点:

在构造函数中传递参数时,gRPC 端点优先于任何配置或环境变量。

from dapr.clients import DaprClient

with DaprClient("mydomain:50051?tls=true") as d:
    # 使用客户端

配置选项:

Dapr Sidecar 端点

您可以使用标准化的 DAPR_GRPC_ENDPOINT 环境变量来指定 gRPC 端点。当设置了此变量时,可以在没有任何参数的情况下初始化客户端:

export DAPR_GRPC_ENDPOINT="mydomain:50051?tls=true"
from dapr.clients import DaprClient

with DaprClient() as d:
    # 客户端将使用环境变量中指定的端点

旧的环境变量 DAPR_RUNTIME_HOSTDAPR_HTTP_PORTDAPR_GRPC_PORT 也被支持,但 DAPR_GRPC_ENDPOINT 优先。

Dapr API 令牌

如果您的 Dapr 实例配置为需要 DAPR_API_TOKEN 环境变量,您可以在环境中设置它,客户端将自动使用它。
您可以在这里阅读更多关于 Dapr API 令牌认证的信息。

健康检查超时

客户端初始化时,会对 Dapr sidecar (/healthz/outbound) 进行健康检查。客户端将在 sidecar 启动并运行后继续。

默认的健康检查超时时间为 60 秒,但可以通过设置 DAPR_HEALTH_TIMEOUT 环境变量来覆盖。

重试和超时

如果从 sidecar 收到特定错误代码,Dapr 客户端可以重试请求。这可以通过 DAPR_API_MAX_RETRIES 环境变量进行配置,并自动获取,不需要任何代码更改。 DAPR_API_MAX_RETRIES 的默认值为 0,这意味着不会进行重试。

您可以通过创建 dapr.clients.retry.RetryPolicy 对象并将其传递给 DaprClient 构造函数来微调更多重试参数:

from dapr.clients.retry import RetryPolicy

retry = RetryPolicy(
    max_attempts=5, 
    initial_backoff=1, 
    max_backoff=20, 
    backoff_multiplier=1.5,
    retryable_http_status_codes=[408, 429, 500, 502, 503, 504],
    retryable_grpc_status_codes=[StatusCode.UNAVAILABLE, StatusCode.DEADLINE_EXCEEDED, ]
)

with DaprClient(retry_policy=retry) as d:
    ...

或对于 actor:

factory = ActorProxyFactory(retry_policy=RetryPolicy(max_attempts=3))
proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface, factory)

超时可以通过环境变量 DAPR_API_TIMEOUT_SECONDS 为所有调用设置。默认值为 60 秒。

注意:您可以通过将 timeout 参数传递给 invoke_method 方法来单独控制服务调用的超时。

错误处理

最初,Dapr 中的错误遵循 标准 gRPC 错误模型。然而,为了提供更详细和信息丰富的错误消息,在版本 1.13 中引入了一个增强的错误模型,与 gRPC 更丰富的错误模型 对齐。作为回应,Python SDK 实现了 DaprGrpcError,一个旨在改善开发者体验的自定义异常类。
需要注意的是,过渡到使用 DaprGrpcError 处理所有 gRPC 状态异常仍在进行中。目前,SDK 中的每个 API 调用尚未更新以利用此自定义异常。我们正在积极进行此增强,并欢迎社区的贡献。

使用 Dapr python-SDK 处理 DaprGrpcError 异常的示例:

try:
    d.save_state(store_name=storeName, key=key, value=value)
except DaprGrpcError as err:
    print(f'状态代码: {err.code()}')
    print(f"消息: {err.message()}")
    print(f"错误代码: {err.error_code()}")
    print(f"错误信息(原因): {err.error_info.reason}")
    print(f"资源信息 (资源类型): {err.resource_info.resource_type}")
    print(f"资源信息 (资源名称): {err.resource_info.resource_name}")
    print(f"错误请求 (字段): {err.bad_request.field_violations[0].field}")
    print(f"错误请求 (描述): {err.bad_request.field_violations[0].description}")

构建块

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

调用服务

Dapr Python SDK 提供了一个简单的 API,用于通过 HTTP 或 gRPC(已弃用)调用服务。可以通过设置 DAPR_API_METHOD_INVOCATION_PROTOCOL 环境变量来选择协议,默认情况下未设置时为 HTTP。Dapr 中的 GRPC 服务调用已弃用,建议使用 GRPC 代理作为替代。

from dapr.clients import DaprClient

with DaprClient() as d:
    # 调用方法 (gRPC 或 HTTP GET)    
    resp = d.invoke_method('service-to-invoke', 'method-to-invoke', data='{"message":"Hello World"}')

    # 对于其他 HTTP 动词,必须指定动词
    # 调用 'POST' 方法 (仅限 HTTP)    
    resp = d.invoke_method('service-to-invoke', 'method-to-invoke', data='{"id":"100", "FirstName":"Value", "LastName":"Value"}', http_verb='post')

HTTP API 调用的基本端点在 DAPR_HTTP_ENDPOINT 环境变量中指定。 如果未设置此变量,则端点值从 DAPR_RUNTIME_HOSTDAPR_HTTP_PORT 变量派生,其默认值分别为 127.0.0.13500

gRPC 调用的基本端点是用于客户端初始化的端点(如上所述)。

保存和获取应用程序状态

from dapr.clients import DaprClient

with DaprClient() as d:
    # 保存状态
    d.save_state(store_name="statestore", key="key1", value="value1")

    # 获取状态
    data = d.get_state(store_name="statestore", key="key1").data

    # 删除状态
    d.delete_state(store_name="statestore", key="key1")

查询应用程序状态 (Alpha)

    from dapr import DaprClient

    query = '''
    {
        "filter": {
            "EQ": { "state": "CA" }
        },
        "sort": [
            {
                "key": "person.id",
                "order": "DESC"
            }
        ]
    }
    '''

    with DaprClient() as d:
        resp = d.query_state(
            store_name='state_store',
            query=query,
            states_metadata={"metakey": "metavalue"},  # 可选
        )

发布和订阅

发布消息

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.publish_event(pubsub_name='pubsub', topic_name='TOPIC_A', data='{"message":"Hello World"}')

订阅消息

from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
import json

app = App()

# 默认订阅一个主题
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A')
def mytopic(event: v1.Event) -> None:
    data = json.loads(event.Data())
    print(f'接收到: id={data["id"]}, message="{data ["message"]}"' 
          ' content_type="{event.content_type}"',flush=True)

# 使用 Pub/Sub 路由的特定处理程序
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A',
               rule=Rule("event.type == \"important\"", 1))
def mytopic_important(event: v1.Event) -> None:
    data = json.loads(event.Data())
    print(f'接收到: id={data["id"]}, message="{data ["message"]}"' 
          ' content_type="{event.content_type}"',flush=True)

流式消息订阅

您可以使用 subscribesubscribe_handler 方法创建对 PubSub 主题的流式订阅。

subscribe 方法返回一个 Subscription 对象,允许您通过调用 next_message 方法从流中提取消息。这将在等待消息时阻塞主线程。完成后,您应该调用 close 方法以终止订阅并停止接收消息。

subscribe_with_handler 方法接受一个回调函数,该函数针对从流中接收到的每条消息执行。它在单独的线程中运行,因此不会阻塞主线程。回调应返回一个 TopicEventResponse(例如 TopicEventResponse('success')),指示消息是否已成功处理、应重试或应丢弃。该方法将根据返回的状态自动管理消息确认。对 subscribe_with_handler 方法的调用返回一个关闭函数,完成后应调用该函数以终止订阅。

以下是使用 subscribe 方法的示例:

import time

from dapr.clients import DaprClient
from dapr.clients.grpc.subscription import StreamInactiveError

counter = 0


def process_message(message):
    global counter
    counter += 1
    # 在此处处理消息
    print(f'处理消息: {message.data()} 来自 {message.topic()}...')
    return 'success'


def main():
    with DaprClient() as client:
        global counter

        subscription = client.subscribe(
            pubsub_name='pubsub', topic='TOPIC_A', dead_letter_topic='TOPIC_A_DEAD'
        )

        try:
            while counter < 5:
                try:
                    message = subscription.next_message()

                except StreamInactiveError as e:
                    print('流不活跃。重试...')
                    time.sleep(1)
                    continue
                if message is None:
                    print('在超时时间内未收到消息。')
                    continue

                # 处理消息
                response_status = process_message(message)

                if response_status == 'success':
                    subscription.respond_success(message)
                elif response_status == 'retry':
                    subscription.respond_retry(message)
                elif response_status == 'drop':
                    subscription.respond_drop(message)

        finally:
            print("关闭订阅...")
            subscription.close()


if __name__ == '__main__':
    main()

以下是使用 subscribe_with_handler 方法的示例:

import time

from dapr.clients import DaprClient
from dapr.clients.grpc._response import TopicEventResponse

counter = 0


def process_message(message):
    # 在此处处理消息
    global counter
    counter += 1
    print(f'处理消息: {message.data()} 来自 {message.topic()}...')
    return TopicEventResponse('success')


def main():
    with (DaprClient() as client):
        # 这将启动一个新线程,该线程将监听消息
        # 并在 `process_message` 函数中处理它们
        close_fn = client.subscribe_with_handler(
            pubsub_name='pubsub', topic='TOPIC_A', handler_fn=process_message,
            dead_letter_topic='TOPIC_A_DEAD'
        )

        while counter < 5:
            time.sleep(1)

        print("关闭订阅...")
        close_fn()


if __name__ == '__main__':
    main()

与输出绑定交互

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.invoke_binding(binding_name='kafkaBinding', operation='create', data='{"message":"Hello World"}')

检索秘密

from dapr.clients import DaprClient

with DaprClient() as d:
    resp = d.get_secret(store_name='localsecretstore', key='secretKey')

配置

获取配置

from dapr.clients import DaprClient

with DaprClient() as d:
    # 获取配置
    configuration = d.get_configuration(store_name='configurationstore', keys=['orderId'], config_metadata={})

订阅配置

import asyncio
from time import sleep
from dapr.clients import DaprClient

async def executeConfiguration():
    with DaprClient() as d:
        storeName = 'configurationstore'

        key = 'orderId'

        # 在 20 秒内等待 sidecar 启动。
        d.wait(20)

        # 通过键订阅配置。
        configuration = await d.subscribe_configuration(store_name=storeName, keys=[key], config_metadata={})
        while True:
            if configuration != None:
                items = configuration.get_items()
                for key, item in items:
                    print(f"订阅键={key} 值={item.value} 版本={item.version}", flush=True)
            else:
                print("尚无内容")
        sleep(5)

asyncio.run(executeConfiguration())

分布式锁

from dapr.clients import DaprClient

def main():
    # 锁参数
    store_name = 'lockstore'  # 在 components/lockstore.yaml 中定义
    resource_id = 'example-lock-resource'
    client_id = 'example-client-id'
    expiry_in_seconds = 60

    with DaprClient() as dapr:
        print('将尝试从名为 [%s] 的锁存储中获取锁' % store_name)
        print('锁是为名为 [%s] 的资源准备的' % resource_id)
        print('客户端标识符是 [%s]' % client_id)
        print('锁将在 %s 秒后过期。' % expiry_in_seconds)

        with dapr.try_lock(store_name, resource_id, client_id, expiry_in_seconds) as lock_result:
            assert lock_result.success, '获取锁失败。中止。'
            print('锁获取成功!!!')

        # 此时锁已释放 - 通过 `with` 子句的魔力 ;)
        unlock_result = dapr.unlock(store_name, resource_id, client_id)
        print('我们已经释放了锁,因此解锁将不起作用。')
        print('我们仍然尝试解锁它,并得到了 [%s]' % unlock_result.status)

加密

from dapr.clients import DaprClient

message = 'The secret is "passw0rd"'

def main():
    with DaprClient() as d:
        resp = d.encrypt(
            data=message.encode(),
            options=EncryptOptions(
                component_name='crypto-localstorage',
                key_name='rsa-private-key.pem',
                key_wrap_algorithm='RSA',
            ),
        )
        encrypt_bytes = resp.read()

        resp = d.decrypt(
            data=encrypt_bytes,
            options=DecryptOptions(
                component_name='crypto-localstorage',
                key_name='rsa-private-key.pem',
            ),
        )
        decrypt_bytes = resp.read()

        print(decrypt_bytes.decode())  # The secret is "passw0rd"

工作流

from dapr.ext.workflow import WorkflowRuntime, DaprWorkflowContext, WorkflowActivityContext
from dapr.clients import DaprClient

instanceId = "exampleInstanceID"
workflowComponent = "dapr"
workflowName = "hello_world_wf"
eventName = "event1"
eventData = "eventData"

def main():
    with DaprClient() as d:
        host = settings.DAPR_RUNTIME_HOST
        port = settings.DAPR_GRPC_PORT
        workflowRuntime = WorkflowRuntime(host, port)
        workflowRuntime = WorkflowRuntime()
        workflowRuntime.register_workflow(hello_world_wf)
        workflowRuntime.register_activity(hello_act)
        workflowRuntime.start()

        # 启动工作流
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # ...

        # 暂停测试
        d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取暂停调用后的响应: {getResponse.runtime_status}")

        # 恢复测试
        d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取恢复调用后的响应: {getResponse.runtime_status}")
        
        sleep(1)
        # 触发事件
        d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

        sleep(5)
        # 清除测试
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

        
        # 启动另一个工作流以进行终止
        # 这也将测试在旧实例被清除后在新工作流上使用相同的实例 ID
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # 终止测试
        d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        sleep(1)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"从 {workflowName} 获取终止调用后的响应: {getResponse.runtime_status}")

        # 清除测试
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

        workflowRuntime.shutdown()

相关链接

Python SDK 示例

6.2 - 使用 Dapr actor Python SDK 入门

如何使用 Dapr Python SDK 快速上手

Dapr actor 包使您能够从 Python 应用程序与 Dapr 虚拟 actor 交互。

先决条件

actor 接口

接口定义了 actor 实现和调用 actor 的客户端之间共享的协议。由于客户端可能依赖于此协议,通常将其定义在与 actor 实现分开的模块中是有意义的。

from dapr.actor import ActorInterface, actormethod

class DemoActorInterface(ActorInterface):
    @actormethod(name="GetMyData")
    async def get_my_data(self) -> object:
        ...

actor 服务

actor 服务负责托管虚拟 actor。它是一个从基类 Actor 派生并实现 actor 接口中定义的类。

可以使用以下 Dapr actor 扩展之一创建 actor:

actor 客户端

actor 客户端用于实现调用 actor 接口中定义的方法。

import asyncio

from dapr.actor import ActorProxy, ActorId
from demo_actor_interface import DemoActorInterface

async def main():
    # 创建代理客户端
    proxy = ActorProxy.create('DemoActor', ActorId('1'), DemoActorInterface)

    # 在客户端上调用方法
    resp = await proxy.GetMyData()

示例

访问此页面获取可运行的 actor 示例。

6.3 - Dapr Python SDK 插件

用于开发 Dapr 应用的 Python SDK 工具

6.3.1 - 开始使用 Dapr Python gRPC 服务扩展

如何启动并运行 Dapr Python gRPC 扩展

Dapr Python SDK 提供了一个用于创建 Dapr 服务的内置 gRPC 服务器扩展 dapr.ext.grpc

安装

您可以通过以下命令下载并安装 Dapr gRPC 服务器扩展:

pip install dapr-ext-grpc
pip3 install dapr-ext-grpc-dev

示例

您可以使用 App 对象来创建一个服务器。

监听服务调用请求

可以使用 InvokeMethodRequestInvokeMethodResponse 对象来处理传入的请求。

以下是一个简单的服务示例,它会监听并响应请求:

from dapr.ext.grpc import App, InvokeMethodRequest, InvokeMethodResponse

app = App()

@app.method(name='my-method')
def mymethod(request: InvokeMethodRequest) -> InvokeMethodResponse:
    print(request.metadata, flush=True)
    print(request.text(), flush=True)

    return InvokeMethodResponse(b'INVOKE_RECEIVED', "text/plain; charset=UTF-8")

app.run(50051)

完整示例可以在这里找到。

订阅主题

在订阅主题时,您可以指示 dapr 事件是否已被接受,或者是否应该丢弃或稍后重试。

from typing import Optional
from cloudevents.sdk.event import v1
from dapr.ext.grpc import App
from dapr.clients.grpc._response import TopicEventResponse

app = App()

# 默认的主题订阅
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A')
def mytopic(event: v1.Event) -> Optional[TopicEventResponse]:
    print(event.Data(), flush=True)
    # 返回 None(或不显式返回)等同于返回 TopicEventResponse("success")。
    # 您还可以返回 TopicEventResponse("retry") 以便 dapr 记录消息并稍后重试交付,
    # 或者返回 TopicEventResponse("drop") 以丢弃消息
    return TopicEventResponse("success")

# 使用发布/订阅路由的特定处理程序
@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A',
               rule=Rule("event.type == \"important\"", 1))
def mytopic_important(event: v1.Event) -> None:
    print(event.Data(), flush=True)

# 禁用主题验证的处理程序
@app.subscribe(pubsub_name='pubsub-mqtt', topic='topic/#', disable_topic_validation=True,)
def mytopic_wildcard(event: v1.Event) -> None:
    print(event.Data(), flush=True)

app.run(50051)

完整示例可以在这里找到。

设置输入绑定触发器

from dapr.ext.grpc import App, BindingRequest

app = App()

@app.binding('kafkaBinding')
def binding(request: BindingRequest):
    print(request.text(), flush=True)

app.run(50051)

完整示例可以在这里找到。

相关链接

6.3.2 - Dapr Python SDK 与 FastAPI 集成指南

如何使用 FastAPI 扩展创建 Dapr Python actor 和发布订阅功能

Dapr Python SDK 通过 dapr-ext-fastapi 扩展实现与 FastAPI 的集成。

安装

您可以通过以下命令下载并安装 Dapr FastAPI 扩展:

pip install dapr-ext-fastapi
pip install dapr-ext-fastapi-dev

示例

订阅不同类型的事件

import uvicorn
from fastapi import Body, FastAPI
from dapr.ext.fastapi import DaprApp
from pydantic import BaseModel

class RawEventModel(BaseModel):
    body: str

class User(BaseModel):
    id: int
    name = 'Jane Doe'

class CloudEventModel(BaseModel):
    data: User
    datacontenttype: str
    id: str
    pubsubname: str
    source: str
    specversion: str
    topic: str
    traceid: str
    traceparent: str
    tracestate: str
    type: str    
    
app = FastAPI()
dapr_app = DaprApp(app)

# 处理任意结构的事件(简单但不够可靠)
# dapr publish --publish-app-id sample --topic any_topic --pubsub pubsub --data '{"id":"7", "desc": "good", "size":"small"}'
@dapr_app.subscribe(pubsub='pubsub', topic='any_topic')
def any_event_handler(event_data = Body()):
    print(event_data)    

# 为了更稳健,根据发布者是否使用 CloudEvents 选择以下之一

# 处理使用 CloudEvents 发送的事件
# dapr publish --publish-app-id sample --topic cloud_topic --pubsub pubsub --data '{"id":"7", "name":"Bob Jones"}'
@dapr_app.subscribe(pubsub='pubsub', topic='cloud_topic')
def cloud_event_handler(event_data: CloudEventModel):
    print(event_data)   

# 处理未使用 CloudEvents 发送的原始事件
# curl -X "POST" http://localhost:3500/v1.0/publish/pubsub/raw_topic?metadata.rawPayload=true -H "Content-Type: application/json" -d '{"body": "345"}'
@dapr_app.subscribe(pubsub='pubsub', topic='raw_topic')
def raw_event_handler(event_data: RawEventModel):
    print(event_data)    

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=30212)

创建一个 actor

from fastapi import FastAPI
from dapr.ext.fastapi import DaprActor
from demo_actor import DemoActor

app = FastAPI(title=f'{DemoActor.__name__}服务')

# 添加 Dapr actor 扩展
actor = DaprActor(app)

@app.on_event("startup")
async def startup_event():
    # 注册 DemoActor
    await actor.register_actor(DemoActor)

@app.get("/GetMyData")
def get_my_data():
    return "{'message': 'myData'}"

6.3.3 - Dapr Python SDK 与 Flask 集成

如何使用 Flask 扩展创建 Dapr Python 虚拟 actor

Dapr Python SDK 使用 flask-dapr 扩展来实现与 Flask 的集成。

安装

您可以通过以下命令下载并安装 Dapr Flask 扩展:

pip install flask-dapr
pip install flask-dapr-dev

示例

from flask import Flask
from flask_dapr.actor import DaprActor

from dapr.conf import settings
from demo_actor import DemoActor

app = Flask(f'{DemoActor.__name__}Service')

# 启用 DaprActor Flask 扩展
actor = DaprActor(app)

# 注册 DemoActor
actor.register_actor(DemoActor)

# 设置方法路由
@app.route('/GetMyData', methods=['GET'])
def get_my_data():
    return {'message': 'myData'}, 200

# 运行应用程序
if __name__ == '__main__':
    app.run(port=settings.HTTP_APP_PORT)

6.3.4 - Dapr Python SDK 与 Dapr Workflow 扩展集成

如何使用 Dapr Workflow 扩展快速上手

Dapr Python SDK 内置了一个 Dapr Workflow 扩展,dapr.ext.workflow,用于创建 Dapr 服务。

安装

您可以通过以下命令下载并安装 Dapr Workflow 扩展:

pip install dapr-ext-workflow
pip3 install dapr-ext-workflow-dev

下一步

开始使用 Dapr Workflow Python SDK

6.3.4.1 - 使用 Dapr Workflow Python SDK 入门

如何使用 Dapr Python SDK 开始并运行工作流

我们来创建一个 Dapr 工作流,并通过控制台调用它。通过提供的 hello world 工作流示例,您将会:

此示例使用 dapr init 的默认配置在本地模式下运行。

在 Python 示例项目中,app.py 文件包含应用程序的设置,其中包括:

  • 工作流定义
  • 工作流活动定义
  • 工作流和工作流活动的注册

先决条件

设置环境

运行以下命令以安装使用 Dapr Python SDK 运行此工作流示例的必要依赖。

pip3 install -r demo_workflow/requirements.txt

克隆 [Python SDK 仓库]。

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

从 Python SDK 根目录导航到 Dapr 工作流示例。

cd examples/demo_workflow

本地运行应用程序

要运行 Dapr 应用程序,您需要启动 Python 程序和一个 Dapr 辅助进程。在终端中运行:

dapr run --app-id orderapp --app-protocol grpc --dapr-grpc-port 50001 --resources-path components --placement-host-address localhost:50005 -- python3 app.py

注意: 由于 Windows 中未定义 Python3.exe,您可能需要使用 python app.py 而不是 python3 app.py

预期输出

== APP == ==========根据输入开始计数器增加==========

== APP == start_resp exampleInstanceID

== APP == 你好,计数器!
== APP == 新的计数器值是:1!

== APP == 你好,计数器!
== APP == 新的计数器值是:11!

== APP == 你好,计数器!
== APP == 你好,计数器!
== APP == 在暂停调用后从 hello_world_wf 获取响应:已暂停

== APP == 你好,计数器!
== APP == 在恢复调用后从 hello_world_wf 获取响应:运行中

== APP == 你好,计数器!
== APP == 新的计数器值是:111!

== APP == 你好,计数器!
== APP == 实例成功清除

== APP == start_resp exampleInstanceID

== APP == 你好,计数器!
== APP == 新的计数器值是:1112!

== APP == 你好,计数器!
== APP == 新的计数器值是:1122!

== APP == 在终止调用后从 hello_world_wf 获取响应:已终止
== APP == 在终止调用后从 child_wf 获取响应:已终止
== APP == 实例成功清除

发生了什么?

当您运行 dapr run 时,Dapr 客户端:

  1. 注册了工作流 (hello_world_wf) 及其活动 (hello_act)
  2. 启动了工作流引擎
def main():
    with DaprClient() as d:
        host = settings.DAPR_RUNTIME_HOST
        port = settings.DAPR_GRPC_PORT
        workflowRuntime = WorkflowRuntime(host, port)
        workflowRuntime = WorkflowRuntime()
        workflowRuntime.register_workflow(hello_world_wf)
        workflowRuntime.register_activity(hello_act)
        workflowRuntime.start()

        print("==========根据输入开始计数器增加==========")
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

然后 Dapr 暂停并恢复了工作流:

       # 暂停
        d.pause_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在暂停调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

        # 恢复
        d.resume_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在恢复调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

一旦工作流恢复,Dapr 触发了一个工作流事件并打印了新的计数器值:

        # 触发事件
        d.raise_workflow_event(instance_id=instanceId, workflow_component=workflowComponent,
                    event_name=eventName, event_data=eventData)

为了从您的状态存储中清除工作流状态,Dapr 清除了工作流:

        # 清除
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

然后示例演示了通过以下步骤终止工作流:

  • 使用与已清除工作流相同的 instanceId 启动一个新的工作流。
  • 在关闭工作流之前终止并清除工作流。
        # 启动另一个工作流
        start_resp = d.start_workflow(instance_id=instanceId, workflow_component=workflowComponent,
                        workflow_name=workflowName, input=inputData, workflow_options=workflowOptions)
        print(f"start_resp {start_resp.instance_id}")

        # 终止
        d.terminate_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        sleep(1)
        getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        print(f"在终止调用后从 {workflowName} 获取响应:{getResponse.runtime_status}")

        # 清除
        d.purge_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        try:
            getResponse = d.get_workflow(instance_id=instanceId, workflow_component=workflowComponent)
        except DaprInternalError as err:
            if nonExistentIDError in err._message:
                print("实例成功清除")

下一步

7 - Dapr Rust SDK

用于开发Dapr应用的Rust SDK包

这是一个帮助开发者使用Rust构建Dapr应用的客户端库。该客户端旨在支持所有公共的Dapr API,同时注重提供符合Rust习惯的开发体验和提升开发者的工作效率。

客户端

使用Rust客户端SDK调用公共的Dapr API [**了解更多关于Rust客户端SDK的信息**](https://v1-16.docs.dapr.io/zh-hans/developing-applications/sdks/rust/rust-client/)

7.1 - 使用 Dapr 客户端 Rust SDK 入门

如何使用 Dapr Rust SDK 快速上手

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

前提条件

引入客户端库

在您的 cargo.toml 文件中添加 Dapr

[dependencies]
# 其他依赖项
dapr = "0.13.0"

您可以引用 dapr::Client,或者将其完整路径绑定到一个新名称,如下所示:

use dapr::Client as DaprClient

实例化 Dapr 客户端

const addr: String = "https://127.0.0.1";
const port: String = "50001";

let mut client = dapr::Client::<dapr::client::TonicClient>::connect(addr,
    port).await?;

功能模块

Rust SDK 允许您与 Dapr 功能模块 进行交互。

服务调用

要在运行 Dapr sidecar 的另一个服务上调用特定方法,Dapr 客户端 Go SDK 提供了以下选项:

调用服务

let response = client
    .invoke_service("service-to-invoke", "method-to-invoke", Some(data))
    .await
    .unwrap();

有关服务调用的完整指南,请访问 如何:调用服务

状态管理

Dapr 客户端提供对状态管理方法的访问:save_stateget_statedelete_state,可以像这样使用:

let store_name = "store-name";
let state_key = "state-key";

let states = vec![(state_key, ("state-value").as_bytes().to_vec())];

// 使用键 "state-key" 和值 "state-value" 保存状态
client.save_state(store_name, states).await?;

// 获取键 "state-key" 的状态
let response = client.get_state(store_name, state_key, None).await.unwrap();

// 删除键 "state-key" 的状态
client.delete_state(store_name, state_key, None).await?;

注意: save_state 方法目前执行的是批量保存,但未来可能会进行重构

有关状态管理的完整指南,请访问 如何:保存和获取状态

发布消息

要将数据发布到主题上,Dapr Go 客户端提供了一种简单的方法:

let pubsub_name = "pubsub-name".to_string();
let pubsub_topic = "topic-name".to_string();
let pubsub_content_type = "text/plain".to_string();

let data = "content".to_string().into_bytes();
client
    .publish_event(pubsub_name, pubsub_topic, pubsub_content_type, data, None)
    .await?;

有关发布/订阅的完整指南,请访问 如何:发布和订阅

相关链接

Rust SDK 示例

8 - Dapr SDK中的序列化

Dapr如何在SDK中序列化数据

Dapr的SDK应该提供两种用例的序列化功能。首先是通过请求和响应负载发送的API对象。其次是需要持久化的对象。对于这两种用例,SDK提供了默认的序列化。在Java SDK中,使用DefaultObjectSerializer类来进行JSON序列化。

服务调用

    DaprClient client = (new DaprClientBuilder()).build();
    client.invokeService("myappid", "saySomething", "My Message", HttpExtension.POST).block();

在上面的示例中,应用程序会收到一个针对saySomething方法的POST请求,请求负载为"My Message" - 引号是因为序列化器会将输入字符串序列化为JSON格式。

POST /saySomething HTTP/1.1
Host: localhost
Content-Type: text/plain
Content-Length: 12

"My Message"

状态管理

    DaprClient client = (new DaprClientBuilder()).build();
    client.saveState("MyStateStore", "MyKey", "My Message").block();

在此示例中,My Message将被保存。它没有加引号,因为Dapr的API会在内部解析JSON请求对象后再保存它。

[
    {
        "key": "MyKey",
        "value": "My Message"
    }
]

发布订阅

  DaprClient client = (new DaprClientBuilder()).build();
  client.publishEvent("TopicName", "My Message").block();

事件被发布,内容被序列化为byte[]并发送到Dapr sidecar。订阅者将以CloudEvent的形式接收它。CloudEvent定义data为字符串。Dapr SDK还为CloudEvent对象提供了内置的反序列化器。

  @PostMapping(path = "/TopicName")
  public void handleMessage(@RequestBody(required = false) byte[] body) {
      // Dapr的事件符合CloudEvent。
      CloudEvent event = CloudEvent.deserialize(body);
  }

绑定

在这种情况下,对象也被序列化为byte[],输入绑定接收原始的byte[]并将其反序列化为预期的对象类型。

  • 输出绑定:
    DaprClient client = (new DaprClientBuilder()).build();
    client.invokeBinding("sample", "My Message").block();
  • 输入绑定:
  @PostMapping(path = "/sample")
  public void handleInputBinding(@RequestBody(required = false) byte[] body) {
      String message = (new DefaultObjectSerializer()).deserialize(body, String.class);
      System.out.println(message);
  }

它应该打印:

My Message

actor方法调用

actor方法调用的对象序列化和反序列化与服务方法调用相同,唯一的区别是应用程序不需要手动反序列化请求或序列化响应,因为这些操作都由SDK自动完成。

对于actor的方法,SDK仅支持具有零个或一个参数的方法。

  • 调用actor的方法:
public static void main() {
    ActorProxyBuilder builder = new ActorProxyBuilder("DemoActor");
    String result = actor.invokeActorMethod("say", "My Message", String.class).block();
}
  • 实现actor的方法:
public String say(String something) {
  System.out.println(something);
  return "OK";
}

它应该打印:

    My Message

actor的状态管理

actor也可以有状态。在这种情况下,状态管理器将使用状态序列化器来序列化和反序列化对象,并自动处理这些操作。

public String actorMethod(String message) {
    // 从键读取状态并将其反序列化为字符串。
    String previousMessage = super.getActorStateManager().get("lastmessage", String.class).block();

    // 在序列化后为键设置新状态。
    super.getActorStateManager().set("lastmessage", message).block();
    return previousMessage;
}

默认序列化器

Dapr的默认序列化器是一个JSON序列化器,具有以下期望:

  1. 使用基本的JSON数据类型以实现跨语言和跨平台的兼容性:字符串、数字、数组、布尔值、null和另一个JSON对象。应用程序可序列化对象中的每个复杂属性类型(例如DateTime)都应表示为JSON的基本类型之一。
  2. 使用默认序列化器持久化的数据也应保存为JSON对象,没有额外的引号或编码。下面的示例显示了字符串和JSON对象在Redis存储中的样子。
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
"This is a message to be saved and retrieved."
 redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
{"value":"My data value."}
  1. 自定义序列化器必须将对象序列化为byte[]
  2. 自定义序列化器必须将byte[]反序列化为对象。
  3. 当用户提供自定义序列化器时,它应作为byte[]传输或持久化。持久化时,也要编码为Base64字符串。这是大多数JSON库本地完成的。
redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||message
"VGhpcyBpcyBhIG1lc3NhZ2UgdG8gYmUgc2F2ZWQgYW5kIHJldHJpZXZlZC4="
 redis-cli MGET "ActorStateIT_StatefulActorService||StatefulActorTest||1581130928192||mydata
"eyJ2YWx1ZSI6Ik15IGRhdGEgdmFsdWUuIn0="

截至目前,Java SDK是唯一实现此规范的Dapr SDK。在不久的将来,其他SDK也将实现相同的功能。