How to build a Dapr’s Pluggable Component

Solopreneur
5 min readJan 8, 2025

--

Pluggable components are components that are not included as part the runtime, as opposed to the built-in components included with dapr init. You can configure Dapr to use pluggable components that leverage the building block APIs, but are registered differently from the built-in Dapr components.

Pluggable components vs. built-in components

Dapr provides two approaches for registering and creating components:

  • The built-in components included in the runtime and found in the components-contrib repository .
  • Pluggable components which are deployed and registered independently.

While both registration options leverage Dapr’s building block APIs, each has a different implementation processes.

Why Use Pluggable Components?

  • Flexibility: Dapr provides the flexibility to integrate with different technologies and services without needing to change the core application code. You can plug in your preferred infrastructure components.
  • Customizability: You can tailor the behavior of Dapr to your specific needs by swapping components (e.g., using a different message broker or storage solution).
  • Portability: With pluggable components, Dapr can work across different cloud providers or on-premises infrastructures, ensuring that your microservices are portable.
  • Decoupling: Pluggable components allow for loose coupling between your application and the underlying infrastructure, enabling you to change technologies or systems without impacting the application code.

Building my first pluggable component

I want to build a demo app which is acting as a dapr state store, which gives you ability to simply create, retrieve and delete records.

  1. I create MyStateStore which will implement IStateStore interface from Dapr.
    internal sealed class MyStateStore : IStateStore
{
private readonly ILogger<MyStateStore> _logger;
private readonly static IDictionary<string, string?> Storage = new ConcurrentDictionary<string, string?>();
public MyStateStore(ILogger<MyStateStore> logger)
{
_logger = logger;
}

public Task DeleteAsync(StateStoreDeleteRequest request, CancellationToken cancellationToken = default)
{
this._logger.LogInformation("Remove request for key {key}", request.Key);
Storage.Remove(request.Key);
return Task.FromResult(new SetResponse());
}

public Task<StateStoreGetResponse?> GetAsync(StateStoreGetRequest request, CancellationToken cancellationToken = default)
{
this._logger.LogInformation("Get request for key {key}", request.Key);
if (Storage.TryGetValue(request.Key, out var data))
{
return Task.FromResult(new StateStoreGetResponse
{
Data = Encoding.ASCII.GetBytes(data),
});
}
return Task.FromResult(new StateStoreGetResponse { });
}

public Task InitAsync(MetadataRequest request, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Init request for quickstore");
return Task.FromResult(new InitResponse { });
}

public Task SetAsync(StateStoreSetRequest request, CancellationToken cancellationToken = default)
{
this._logger.LogInformation("Set request for key {key}", request.Key);
Storage[request.Key] = Encoding.UTF8.GetString(request.Value.Span);
return Task.FromResult(new SetResponse());
}
}

2. I want to register MyStateStore as a service, in program.cs

using Dapr.PluggableComponents;
using quickstore;

var app = DaprPluggableComponentsApplication.Create();

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

app.Run();

3. Create the docker file for this application, so it can be containerized.

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY . ./
RUN dotnet restore "./quickstore.csproj"

RUN dotnet build "./quickstore.csproj" -c Release -o /app/build

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

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

4. You will need to define the state component schema and save as yaml file which we will use later.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.quickstore
version: v1
initTimeout: 1m
metadata:
- name: quickstore

5. To test dapr, we need to create a simple dapr-client. I made it simple enough to use DaprClient to save and read dapr states via the “statestore” I created.

    class Program
{
static async Task Main(string[] args)
{
string DAPR_STORE_NAME = "statestore";
var clientBuilder = new DaprClientBuilder();
// var port = 50002;
// clientBuilder = clientBuilder.UseGrpcEndpoint($"http://localhost:{port}");
//clientBuilder = clientBuilder($"http://localhost");

using var client = clientBuilder.Build();
while (true)
{
System.Threading.Thread.Sleep(5000);
Random random = new Random();
int orderId = random.Next(1, 1000);
//Using Dapr SDK to save and get state
await client.SaveStateAsync(DAPR_STORE_NAME, "order_1", orderId.ToString());
await client.SaveStateAsync(DAPR_STORE_NAME, "order_2", orderId.ToString());
var result = await client.GetStateAsync<string>(DAPR_STORE_NAME, "order_1");
Console.WriteLine("Result after get: " + result);
}
}
}

6. To run this dapr client in standalone mode, you can run this command.

dapr run --app-id orderprocessing --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 dotnet run

7. Alternatively, you can dockerize it with a docker file, so it can be deployed as docker-compose or kubernetes.

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
WORKDIR /FirstDaprStateClient

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /FirstDaprStateClient
COPY --from=build-env /FirstDaprStateClient/out .
ENTRYPOINT ["dotnet", "FirstDaprStateClient.dll"]

8. Now let’s test our pluggable component with our dapr client in a docker-compose. We will need to create the following services:

  • client application
  • dapr
  • quickstore (i.e. my testing pluggable component)
  • placement
version: '3'
services:
client:
build:
context: .
dockerfile: FirstDaprStateClient/Dockerfile
ports:
- "50002:50002"
depends_on:
- quickstore
- placement
networks:
- hello-dapr
dapr:
image: "daprio/daprd:edge"
command: [
"./daprd",
"--app-id", "client",
"--app-port", "3000",
"--dapr-grpc-port", "50002",
"--components-path", "/components",
"--placement-host-address", "placement:50006", # Dapr's placement service can be reach via the docker DNS entry
"--log-level", "debug",
]
volumes:
- "./components:/components"
- "/tmp/dapr-components-sockets:/tmp/dapr-components-sockets"
depends_on:
- client
- quickstore
network_mode: "service:client" # Attach the nodeapp-dapr service to the nodeapp network namespace

quickstore:
build:
context: quickstore
dockerfile: Dockerfile
volumes:
- "/tmp/dapr-components-sockets:/tmp/dapr-components-sockets"
networks:
- hello-dapr

placement:
image: "daprio/dapr"
command: ["./placement", "--port", "50006"]
ports:
- "50006:50006"
depends_on:
- quickstore
networks:
- hello-dapr

networks:
hello-dapr: null

9. When you run docker-compose up, you would see the dapr client is interacting with dapr statestore that we created in the logs.

dockcer-compose up

10. If you want to run it in kubernetes, I have pre-installed minikube locally to test it.

apiVersion: apps/v1
kind: Deployment
metadata:
name: firstdaprstateclient
labels:
app: firstdaprstateclient
spec:
replicas: 1
selector:
matchLabels:
app: firstdaprstateclient
template:
metadata:
labels:
app: firstdaprstateclient
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "firstdaprstateclient"
#dapr.io/app-port: "80"
#dapr.io/dapr-grpc-port: "50002"
dapr.io/enable-api-logging: "true"
dapr.io/unix-domain-socket-path: /tmp/dapr-components-sockets
#dapr-unix-domain-socket
spec:
containers:
- name: consumer-app
image: superwalnut/dapr-client-demo:latest
volumeMounts:
- name: dapr-unix-domain-socket
mountPath: /tmp/dapr-components-sockets
ports:
- containerPort: 80
imagePullPolicy: Always

- name: quickstore
image: superwalnut/dapr-component:latest
command: ["dotnet", "quickstore.dll"]
env:
- name: DAPR_GRPC_PORT
value: "50001"
- name: DAPR_HTTP_PORT
value: "3500"
ports:
- containerPort: 50001
- containerPort: 3500
volumeMounts:
- name: dapr-unix-domain-socket
mountPath: /tmp/dapr-components-sockets
volumes:
- name: dapr-unix-domain-socket
emptyDir: {}

With Dapr’s pluggable components allow developers to easily customize how their microservices interact with external systems, providing flexibility, portability, and ease of integration with various cloud-native services.

These components are an essential part of Dapr’s modular design and help developers choose the right technologies for their applications while abstracting away much of the complexity involved in integrating with those systems.

--

--

Solopreneur
Solopreneur

Written by Solopreneur

I am exploring web dev trends in areas like .net, serverless, react, angular, etc. & using medium as an amazing platform to improve my skills and share my code

No responses yet