Development Guide
Guide for building, testing, and running the Virtufin WorkManager locally.
Table of Contents
- Prerequisites
- Project Structure
- Building
- Running Locally
- Testing
- Debugging
- Generating Protobuf Files
- Troubleshooting
Prerequisites
Required Software
| Software | Version | Purpose |
|---|---|---|
| .NET SDK | 10.0+ | Runtime and build |
| Dapr CLI | 1.18+ | Local development sidecar |
| Docker | Latest | Redis/Valkey for state + pub/sub |
| Python | 3.9+ | Python engine workers (subprocess) |
| buf | 1.40+ | Protobuf code generation (optional) |
Install .NET SDK
# macOS
brew install dotnet
# Linux (Ubuntu)
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install dotnet-sdk-10.0
# Windows
winget install Microsoft.DotNet.SDK.10
Install Dapr CLI
# macOS
brew install dapr/tap/dapr
# Linux
curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | bash
# Initialize Dapr (downloads placement, redis-placement, and zipkin containers)
dapr init
Project Structure
src/
├── Virtufin.WorkManager/ # Main application
│ ├── Configuration/ # Options + PortConstants
│ ├── Engines/ # IEngine + IAsyncDisposable
│ ├── HostedServices/ # WorkManagerRecovery
│ ├── Services/ # gRPC service + Dapr
│ ├── Runtime/ # Process orchestration
│ └── Program.cs
├── Virtufin.WorkManager.Protos/ # Proto definitions
├── Virtufin.WorkManager.Client/ # .NET client NuGet library
├── Virtufin.WorkManager.Engine.Python/ # Python engine (subprocess)
├── Virtufin.WorkManager.Engine.CSharpSource/ # C# source engine (Roslyn)
├── Virtufin.WorkManager.Engine.DotNetDll/ # .NET DLL engine (hostfxr + ALC)
tests/
├── Virtufin.WorkManager.Tests/ # Service-level tests
├── Virtufin.WorkManager.Client.Tests/ # Client library tests
├── python/ # Python client tests + DevKit tests
└── typescript/ # TypeScript client tests
Building
# Build the entire solution
dotnet build Virtufin.WorkManager.slnx
# Build a specific project
dotnet build src/Virtufin.WorkManager/Virtufin.WorkManager.csproj
The build output is placed in src/*/bin/Debug/net10.0/.
Running Locally
1. Start Dapr and Redis
dapr init # one-time
docker compose up -d redis
2. Run the WorkManager
cd src/Virtufin.WorkManager
DAPR_COMPONENTS_PATH=/path/to/deploy-local/components # set to your path
dapr run --app-id workmanager \
--app-protocol grpc \
--app-port 5102 \
--dapr-http-port 3500 \
--resources-path "$DAPR_COMPONENTS_PATH" \
-- dotnet run
Clone the upstream
virtufin/deploy-local
repo (or copy its components/ directory to any path) to obtain
the Dapr component definitions (pubsub, statestore, config).
For convenience, if you cloned deploy-local as a sibling of
virtufin-workmanager/, the path becomes
../deploy-local/components.
The service exposes:
- gRPC on
:5102(with reflection enabled) - HTTP/REST + Swagger on
:5001
3. Verify
# Health check
curl http://localhost:5001/health
# Swagger UI
open http://localhost:5001/swagger
# gRPC reflection service list
grpcurl -plaintext localhost:5102 list
Testing
Run All Tests
dotnet test Virtufin.WorkManager.slnx
Run Tests with Coverage
dotnet test Virtufin.WorkManager.slnx \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
Run Specific Tests
# By class name
dotnet test --filter "FullyQualifiedName~PythonEngineTests"
# By trait/category
dotnet test --filter "Category=Integration"
Test Projects
| Test Project | Description |
|---|---|
Virtufin.WorkManager.Tests |
Service-level tests (engines, runtime, recovery) |
Virtufin.WorkManager.Client.Tests |
Client library tests (hand-written + generated) |
tests/python/ |
Python client + DevKit tests |
tests/typescript/ |
TypeScript client tests |
Writing Tests
- Place unit tests next to the class they test in
*.Tests/ - Engine tests use mock I/O — they do not spawn real Python subprocesses
- Runtime tests use
WebApplicationFactory<Program>for in-process integration testing
Debugging
Visual Studio Code
.vscode/launch.json includes a debug profile that runs the WorkManager under Dapr:
# In VS Code
code .
# Press F5 → "Launch WorkManager (Dapr)" profile
JetBrains Rider
Rider auto-detects the launchSettings profile. Right-click Virtufin.WorkManager → "Run/Debug" → choose the "Dapr" launch profile.
Common Debug Scenarios
- Python worker not starting: set a breakpoint in
PythonEngine.StartProcessAsyncand verify theAcceptTcpClientAsynccall completes - Worker recovery failing: check
WorkManagerRecoveryHostedServicelogs and the state store contents (docker exec -it dapr_redis_1 redis-cli KEYS '*') - gRPC serialization errors: enable
EnableDetailedErrorsinProgram.cs(gated onIsDevelopment())
Generating Protobuf Files
Proto files live in src/Virtufin.WorkManager.Protos/proto/. To regenerate stubs after editing a .proto file:
cd src/Virtufin.WorkManager.Protos/proto
buf generate
This regenerates:
- C# stubs →
src/Virtufin.WorkManager/Generated/ - Python stubs →
src/python/virtufin/workmanager/workmanager_pb2*.py - TypeScript stubs →
src/typescript/src/generated/workmanager_pb.js
Generated files are excluded from git.
Regenerate Client Tests
After regenerating stubs, also regenerate the cross-language client tests:
# The repos are NOT a monorepo, so the script is fetched
# directly from the virtufin-common Gitea repo at runtime.
python3 <(curl -L https://git.haenerconsulting.com/virtufin/virtufin-common/raw/branch/master/scripts/generate-client-tests.py) \
src/Virtufin.WorkManager.Protos/proto/workmanager.proto \
--prefix M
Troubleshooting
"Python worker process exited with code N"
Check the Python engine logs for the StartProcessAsync call. Common causes:
- Python 3 not on PATH — the engine calls
EnsurePythonAvailableAsyncwith a 10s timeout; verify withpython3 --version - Sandbox module blocked — the worker's
importstatement tries to load a blocked module (seePYTHON_BLOCKED_MODULESconfig) - Stdin closed — the worker process may have crashed; check stderr capture in the logs
"Dapr sidecar not responding"
Check that Dapr is initialized and the placement service is running:
dapr status
docker ps | grep dapr
If dapr_redis-placement_1 is not running, restart Dapr: dapr init --slim.
"Worker not found" after restart
The recovery hosted service re-creates workers from state on startup. If a worker is missing:
- Verify the state store contains the worker record:
docker exec -it dapr_redis_1 redis-cli KEYS 'worker|*' - Check
WorkManagerRecoveryHostedServicelogs forLoadAsyncerrors - Force a clean state:
dapr stop --app-id workmanager && docker volume prune
Port conflicts
If 5001/5102 are in use, override via env vars:
HttpPort=5001 GrpcPort=5102 dapr run --app-port 5102 -- dotnet run
Or command-line switches:
dotnet run -- --http-port 6001 --grpc-port 6102
Memory leaks (long-running workers)
Each worker holds an engine instance with a Python subprocess. If the worker count grows without bound, check:
- The reclaim hosted service is running and reclaiming dead workers
- The state store TTL on worker records is set (recommended: 24h)
- Your application code is calling
DeleteWorkerwhen workers are no longer needed