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

Return to the regular view of this page.

Dapr 可插拔组件 .NET SDK 入门

如何开始使用 Dapr 可插拔组件 .NET SDK

Dapr 提供 NuGet 包以帮助开发 .NET 可插拔组件。

前置条件

创建项目

创建可插拔组件始于一个空的 ASP.NET 项目。

dotnet new web --name <project name>

添加 NuGet 包

添加 Dapr .NET 可插拔组件 NuGet 包。

dotnet add package Dapr.PluggableComponents.AspNetCore

创建应用和服务

创建 Dapr 可插拔组件应用类似于创建 ASP.NET 应用。在 Program.cs 中,将 WebApplication 相关代码替换为 Dapr 等效的 DaprPluggableComponentsApplication

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        // Register one or more components with this service.
    });

app.Run();

这将创建一个包含单个服务的应用。每个服务:

  • 对应单个 Unix 域套接字
  • 可以承载一种或多种组件类型

实现和注册组件

本地测试组件

可以通过在命令行启动应用并配置 Dapr 边车来使用它,从而测试可插拔组件。

要启动组件,在应用目录中:

dotnet run

要配置 Dapr 使用该组件,在资源路径目录中:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: <component name>
spec:
  type: state.<socket name>
  version: v1
  metadata:
  - name: key1
    value: value1
  - name: key2
    value: value2

当组件实例化时,任何 metadata 属性都将通过其 IPluggableComponent.InitAsync() 方法传递给组件。

要启动 Dapr(以及可选的,使用该服务的服务):

dapr run --app-id <app id> --resources-path <resources path> ...

此时,Dapr 边车将启动并通过 Unix 域套接字连接到组件。然后您可以通过以下任一方式与组件交互:

  • 通过使用该组件的服务(如果已启动),或
  • 直接使用 Dapr HTTP 或 gRPC API

创建容器

有多种方法可以为您的组件创建容器以进行最终部署。

使用 .NET SDK

.NET 7 及更高版本的 SDK 使您能够在使用 Dockerfile 的情况下为应用创建基于 .NET 的容器,即使是针对早期版本的 .NET SDK 的应用。这可能是目前为组件生成容器的最简单方法。

Microsoft.NET.Build.Containers NuGet 包添加到组件项目。

dotnet add package Microsoft.NET.Build.Containers

将应用发布为容器:

dotnet publish --os linux --arch x64 /t:PublishContainer -c Release

有关更多配置选项,例如控制容器名称、标签和基础镜像,请参阅 .NET 发布为容器指南

使用 Dockerfile

虽然有工具可以为 .NET 应用生成 Dockerfile,但 .NET SDK 本身不会。典型的 Dockerfile 可能如下所示:

FROM mcr.microsoft.com/dotnet/aspnet:<runtime> AS base
WORKDIR /app

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

FROM mcr.microsoft.com/dotnet/sdk:<runtime> AS build
WORKDIR /src
COPY ["<application>.csproj", "<application folder>/"]
RUN dotnet restore "<application folder>/<application>.csproj"
COPY . .
WORKDIR "/src/<application folder>"
RUN dotnet build "<application>.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "<application>.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "<application>.dll"]

构建镜像:

docker build -f Dockerfile -t <image name>:<tag> .

演示

观看此视频,了解 使用 .NET 构建可插拔组件的演示

后续步骤

1 - 实现 .NET 输入/输出绑定组件

如何使用 Dapr 可插拔组件 .NET SDK 创建输入/输出绑定

创建绑定组件只需要几个基本步骤。

添加绑定命名空间

添加绑定相关命名空间的 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.Bindings;

输入绑定:实现 IInputBinding

创建一个实现 IInputBinding 接口的类。

internal sealed class MyBinding : IInputBinding
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 使用配置的元数据初始化组件时调用...
    }

    public async Task ReadAsync(MessageDeliveryHandler<InputBindingReadRequest, InputBindingReadResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        // 直到被取消之前,检查底层存储中的消息并将其传递给 Dapr 运行时...
    }
}

ReadAsync() 方法的调用是"长期运行"的,因为该方法预期在取消之前不会返回(例如,通过 cancellationToken)。当从组件的底层存储读取消息时,这些消息通过 deliveryHandler 回调传递给 Dapr 运行时。传递操作允许组件在应用程序(由 Dapr 运行时服务)确认处理消息时接收通知。

    public async Task ReadAsync(MessageDeliveryHandler<InputBindingReadRequest, InputBindingReadResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        TimeSpan pollInterval = // 轮询间隔(例如,来自初始化元数据)...

        // 轮询底层存储直到被取消...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从底层存储轮询消息...

            foreach (var message in messages)
            {
                // 将消息传递给 Dapr 运行时...
                await deliveryHandler(
                    new InputBindingReadResponse
                    {
                        // 设置消息内容...
                    },
                    // 当应用程序确认消息时调用的回调...
                    async request =>
                    {
                        // 处理响应数据或错误消息...
                    })
            }

            // 等待下一次轮询(或取消)...
            await Task.Delay(pollInterval, cancellationToken);
        }
    }

输出绑定:实现 IOutputBinding

创建一个实现 IOutputBinding 接口的类。

internal sealed class MyBinding : IOutputBinding
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 使用配置的元数据初始化组件时调用...
    }

    public Task<OutputBindingInvokeResponse> InvokeAsync(OutputBindingInvokeRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以执行特定操作...
    }

    public Task<string[]> ListOperationsAsync(CancellationToken cancellationToken = default)
    {
        // 调用以列出可执行的操作。
    }
}

输入和输出绑定组件

组件可以同时是输入和输出绑定,只需实现这两个接口即可。

internal sealed class MyBinding : IInputBinding, IOutputBinding
{
    // IInputBinding 实现...

    // IOutputBinding 实现...
}

注册绑定组件

在主程序文件(例如 Program.cs)中,在应用程序服务中注册绑定组件。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterBinding<MyBinding>();
    });

app.Run();

后续步骤

2 - 实现 .NET 发布订阅组件

如何使用 Dapr 可插拔组件 .NET SDK 创建发布订阅

创建发布订阅组件只需几个基本步骤。

添加发布订阅命名空间

为发布订阅相关的命名空间添加 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.PubSub;

实现 IPubSub

创建一个实现 IPubSub 接口的类。

internal sealed class MyPubSub : IPubSub
{
    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以使用配置的元数据初始化组件...
    }

    public Task PublishAsync(PubSubPublishRequest request, CancellationToken cancellationToken = default)
    {
        // 将消息发送到"topic"...
    }

    public Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        // 直到取消之前,检查 topic 中的消息并将其传递给 Dapr runtime...
    }
}

PullMessagesAsync() 方法的调用是"长期存在"的,也就是说该方法在取消之前(例如通过 cancellationToken)不会返回。应从中拉取消息的"topic"通过 topic 参数传递,而向 Dapr runtime 的传递则通过 deliveryHandler 回调执行。传递允许组件在应用程序(由 Dapr runtime 提供服务)确认已处理消息时接收通知。

    public async Task PullMessagesAsync(PubSubPullMessagesTopic topic, MessageDeliveryHandler<string?, PubSubPullMessagesResponse> deliveryHandler, CancellationToken cancellationToken = default)
    {
        TimeSpan pollInterval = // 轮询间隔(例如来自初始化元数据)...

        // 轮询 topic 直到取消...
        while (!cancellationToken.IsCancellationRequested)
        {
            var messages = // 从 topic 轮询消息...

            foreach (var message in messages)
            {
                // 将消息传递给 Dapr runtime...
                await deliveryHandler(
                    new PubSubPullMessagesResponse(topicName)
                    {
                        // 设置消息内容...
                    },
                    // 当应用程序确认消息时调用的回调...
                    async errorMessage =>
                    {
                        // 空消息表示应用程序成功处理了消息...
                        if (String.IsNullOrEmpty(errorMessage))
                        {
                            // 从 topic 中删除消息...
                        }
                    })
            }

            // 等待下一次轮询(或取消)...
            await Task.Delay(pollInterval, cancellationToken);
        }
    }

注册发布订阅组件

在主程序文件(例如 Program.cs)中,向应用程序服务注册发布订阅组件。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterPubSub<MyPubSub>();
    });

app.Run();

后续步骤

3 - 实现 .NET 状态存储组件

如何使用 Dapr 可插拔组件 .NET SDK 创建状态存储

创建状态存储组件只需要几个基本步骤。

添加状态存储命名空间

添加状态存储相关命名空间的 using 语句。

using Dapr.PluggableComponents.Components;
using Dapr.PluggableComponents.Components.StateStore;

实现 IStateStore

创建一个实现 IStateStore 接口的类。

internal sealed class MyStateStore : IStateStore
{
    public Task DeleteAsync(StateStoreDeleteRequest request, CancellationToken cancellationToken = default)
    {
        // 从状态存储中删除请求的键...
    }

    public Task<StateStoreGetResponse?> GetAsync(StateStoreGetRequest request, CancellationToken cancellationToken = default)
    {
        // 从状态存储中获取请求的键值,否则返回 null...
    }

    public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
    {
        // 调用以使用配置的元数据初始化组件...
    }

    public Task SetAsync(StateStoreSetRequest request, CancellationToken cancellationToken = default)
    {
        // 在状态存储中设置请求的键为指定值...
    }
}

注册状态存储组件

在主程序文件(例如 Program.cs)中,向应用程序服务注册状态存储。

using Dapr.PluggableComponents;

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "<socket name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

批量状态存储

旨在支持批量操作的状态存储应实现可选的 IBulkStateStore 接口。其方法镜像了基础 IStateStore 接口的方法,但包含多个请求值。

internal sealed class MyStateStore : IStateStore, IBulkStateStore
{
    // ...

    public Task BulkDeleteAsync(StateStoreDeleteRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 从状态存储中删除所有请求的值...
    }

    public Task<StateStoreBulkStateItem[]> BulkGetAsync(StateStoreGetRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 从状态存储中返回所有请求的值...
    }

    public Task BulkSetAsync(StateStoreSetRequest[] requests, CancellationToken cancellationToken = default)
    {
        // 在状态存储中设置所有请求的键的值...
    }
}

事务状态存储

旨在支持事务的状态存储应实现可选的 ITransactionalStateStore 接口。其 TransactAsync() 方法接收一个请求,其中包含要在事务中执行的一系列删除和/或设置操作。状态存储应遍历该序列并调用每个操作的 Visit() 方法,传入代表对每种操作类型要执行的操作的回调。

internal sealed class MyStateStore : IStateStore, ITransactionalStateStore
{
    // ...

    public async Task TransactAsync(StateStoreTransactRequest request, CancellationToken cancellationToken = default)
    {
        // 开始事务...

        try
        {
            foreach (var operation in request.Operations)
            {
                await operation.Visit(
                    async deleteRequest =>
                    {
                        // 处理删除请求...

                    },
                    async setRequest =>
                    {
                        // 处理设置请求...
                    });
            }
        }
        catch
        {
            // 回滚事务...

            throw;
        }

        // 提交事务...
    }
}

可查询状态存储

旨在支持查询的状态存储应实现可选的 IQueryableStateStore 接口。其 QueryAsync() 方法接收有关查询的详细信息,例如筛选器、结果限制和分页,以及结果的排序顺序。状态存储应使用这些详细信息生成一组值作为其响应的一部分返回。

internal sealed class MyStateStore : IStateStore, IQueryableStateStore
{
    // ...

    public Task<StateStoreQueryResponse> QueryAsync(StateStoreQueryRequest request, CancellationToken cancellationToken = default)
    {
        // 生成并返回结果...
    }
}

ETag 和其他语义错误处理

Dapr 运行时对某些状态存储操作导致的某些错误条件有额外的处理。状态存储可以通过从其操作逻辑中抛出特定异常来指示此类情况:

异常适用操作描述
ETagInvalidExceptionDelete、Set、Bulk Delete、Bulk Set当 ETag 无效时
ETagMismatchExceptionDelete、Set、Bulk Delete、Bulk Set当 ETag 与预期值不匹配时
BulkDeleteRowMismatchExceptionBulk Delete当受影响的行数与预期行数不匹配时

后续步骤

4 - Dapr 可插拔组件 .NET SDK 的高级用法

如何使用 Dapr 可插拔组件 .NET SDK 的高级技术

尽管大多数人通常不需要,但这些指南展示了配置 .NET 可插拔组件的高级方法。

4.1 - .NET Dapr 可插拔组件中的多个服务

如何从 .NET 可插拔组件公开多个服务

可插拔组件可以托管多种类型的多个组件。您可能需要这样做:

  • 为了最小化集群中运行的边车数量
  • 为了分组可能共享库和实现的相关组件,例如:
    • 一个既作为通用状态存储公开的数据库,和
    • 允许更特定操作的输出绑定。

每个 Unix 域套接字可以管理对每种类型的一个组件的调用。要托管相同类型的多个组件,您可以将这些类型分散到多个套接字上。SDK 将每个套接字绑定到一个"服务",每个服务由一种或多种组件类型组成。

注册多个服务

每次调用 RegisterService() 都会将一个套接字绑定到一组已注册的组件,其中每个服务可以注册每种类型的组件中的一个。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyDatabaseStateStore>();
        serviceBuilder.RegisterBinding<MyDatabaseOutputBinding>();
    });

app.RegisterService(
    "service-b",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<AnotherStateStore>();
    });

app.Run();

class MyDatabaseStateStore : IStateStore
{
    // ...
}

class MyDatabaseOutputBinding : IOutputBinding
{
    // ...
}

class AnotherStateStore : IStateStore
{
    // ...
}

配置多个组件

配置 Dapr 以使用托管组件与任何单个组件相同 - 组件 YAML 引用关联的套接字。

#
# 此组件使用与套接字 `state-store-a` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-a
spec:
  type: state.service-a
  version: v1
  metadata: []
#
# 此组件使用与套接字 `state-store-b` 关联的状态存储
#
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state-store-b
spec:
  type: state.service-b
  version: v1
  metadata: []

后续步骤

4.2 - .NET Dapr 可插拔组件的应用环境

如何配置 .NET 可插拔组件的环境

.NET Dapr 可插拔组件应用可以像 ASP.NET 应用一样配置依赖注入、日志和配置值。DaprPluggableComponentsApplication 暴露了一组与 WebApplicationBuilder 相似的配置属性。

依赖注入

注册到服务的组件可以参与依赖注入。在创建组件时,组件构造函数中的参数将被注入,前提是这些类型已在应用中注册。你可以通过 DaprPluggableComponentsApplication 暴露的 IServiceCollection 注册它们。

var app = DaprPluggableComponentsApplication.Create();

// 将 MyService 注册为 IService 的单例实现。
app.Services.AddSingleton<IService, MyService>();

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

interface IService
{
    // ...
}

class MyService : IService
{
    // ...
}

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入 IService。
    public MyStateStore(IService service)
    {
        // ...
    }

    // ...
}

日志

.NET Dapr 可插拔组件可以使用标准 .NET 日志机制DaprPluggableComponentsApplication 暴露了一个 ILoggingBuilder,可以通过它进行配置。

var app = DaprPluggableComponentsApplication.Create();

// 清除默认日志记录器并设置新的日志记录器。
app.Logging.ClearProviders();
app.Logging.AddConsole();

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入日志记录器。
    public MyStateStore(ILogger<MyStateStore> logger)
    {
        // ...
    }

    // ...
}

配置值

由于 .NET 可插拔组件基于 ASP.NET 构建,它们可以使用其标准配置机制,并默认使用同一组预先注册的提供程序DaprPluggableComponentsApplication 暴露了一个 IConfigurationManager,可以通过它进行配置。

var app = DaprPluggableComponentsApplication.Create();

// 清除默认配置提供程序并添加新的提供程序。
((IConfigurationBuilder)app.Configuration).Sources.Clear();
app.Configuration.AddEnvironmentVariables();

// 在启动时获取配置值。
const value = app.Configuration["<name>"];

app.RegisterService(
    "<service name>",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<MyStateStore>();
    });

app.Run();

class MyStateStore : IStateStore
{
    // 在创建状态存储时注入配置。
    public MyStateStore(IConfiguration configuration)
    {
        // ...
    }

    // ...
}

后续步骤

4.3 - .NET Dapr 可插拔组件的生命周期

如何控制 .NET 可插拔组件的生命周期

有两种方式注册组件:

  • 组件作为单例运行,其生命周期由 SDK 管理
  • 组件的生命周期由可插拔组件决定,可根据需要为多实例或单例

单例组件

_按类型_注册的组件是单例:一个实例将为与该 socket 关联的该类型的所有已配置组件提供服务。当该类型仅存在单个组件且在 Dapr 应用程序之间共享时,此方法最佳。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore<SingletonStateStore>();
    });

app.Run();

class SingletonStateStore : IStateStore
{
    // ...
}

多实例组件

可以通过传递"工厂方法"来注册组件。对于与该 socket 关联的该类型的每个已配置组件,都会调用此方法。该方法返回要与该组件关联的实例(无论是否共享)。当同一类型的多个组件可能使用不同的元数据集进行配置,或需要将组件操作彼此隔离时,此方法最佳。

工厂方法将接收上下文,例如已配置的 Dapr 组件的 ID,可用于区分组件实例。

var app = DaprPluggableComponentsApplication.Create();

app.RegisterService(
    "service-a",
    serviceBuilder =>
    {
        serviceBuilder.RegisterStateStore(
            context =>
            {
                return new MultiStateStore(context.InstanceId);
            });
    });

app.Run();

class MultiStateStore : IStateStore
{
    private readonly string instanceId;

    public MultiStateStore(string instanceId)
    {
        this.instanceId = instanceId;
    }

    // ...
}

后续步骤