DEV Community

Jon Davis
Jon Davis

Posted on

C# (Maybe) Joins the Motia Family: Building Multi-Language Workflows with .NET 9

What is Motia?

If you haven't heard of Motia yet, it's a polyglot backend framework that unifies APIs, background jobs, scheduled tasks, and AI agents into a single runtime. Instead of piecing together Express for APIs, Bull for queues, node-cron for scheduling, and separate platforms for AI agents, you write everything as "Steps" - a simple primitive with four concepts: Trigger (API/event/cron), Handler (your logic), Emit (output events), and Subscribe (input topics).

The magic is that Steps can be written in different languages in the same project. Your API endpoint in TypeScript can emit an event that triggers a Python step (perfect for ML processing), which then emits to a Ruby step, all with built-in observability showing exactly what happened and when. No microservices orchestration headaches, no message broker setup, no distributed tracing configuration - it's all there.

Think React's component model, but for backend workflows. That's the vision.

C# Support

I'm excited to share that C# (.NET 9) support may soon be available in Motia. Following TDD principles, I've built full C# integration that lets developers write backend services alongside TypeScript, Python, JavaScript, and Ruby - all in the same project with automatic cross-language communication.

Try it now: This is currently a PR awaiting review from the Motia maintainers, but you can use it today from my fork:

https://github.com/stimpy77/motia/tree/feat/csharp-dotnet-support

The pull request to the main repo: #769

What Makes This Special?

Unlike traditional polyglot systems that require glue code, this C# integration is truly seamless:

  • Full bidirectional RPC - State operations work across all languages
  • Zero configuration - Just write .step.cs files and go
  • Complete feature set - State management, logging, tracing, all 4 step types
  • Type-safe - Leverage C#'s strong type system
  • Cross-platform compatible (tested on macOS, should work on Linux/Windows)

See It In Action

Here's a simple example - an API endpoint in C# that communicates with event handlers in any language:

// steps/api.step.cs
public static class ApiStepConfig
{
    public static object Config = new
    {
        type = "api",
        name = "OrderAPI",
        path = "/orders",
        method = "POST",
        emits = new[] { "order.created" }
    };
}

public static class ApiStepHandler
{
    public static async Task<object> Handler(object req, dynamic ctx)
    {
        // Set shared state
        await ctx.State.Set("lastOrder", "ORD-12345");

        // Emit event (any language can subscribe!)
        await ctx.Emit(new
        {
            topic = "order.created",
            data = new { orderId = "ORD-12345", amount = 99.99 }
        });

        // Log with automatic tracing
        ctx.Logger.Info("Order created", new { orderId = "ORD-12345" });

        return new
        {
            status = 201,
            body = new { message = "Order created successfully" }
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it! No boilerplate, no configuration files, no infrastructure setup. Just pure business logic.

The Development Journey

The implementation followed strict TDD. Here are the main challenges I tackled:

1. Roslyn Scripting Integration

Getting Roslyn to dynamically compile C# code at runtime while maintaining fast execution was tricky. The challenge was balancing compilation speed (~500-1000ms) with the flexibility of not requiring pre-compiled assemblies. I went with Roslyn scripting over native AOT compilation to keep the developer experience smooth.

2. RPC Protocol Design

C# needed to talk to Node.js over stdin/stdout using JSON-RPC. The initial implementation was one-way (C# sends, Node.js processes) which worked for emit() and State.Set(). But State.Get() requires bidirectional communication - send a request, wait for a response.

The breakthrough was implementing:

  • Request ID tracking with TaskCompletionSource for async/await patterns
  • A background thread continuously reading responses from stdin
  • Response matching to complete the right pending request
  • 30-second timeouts to prevent hanging
// This now works!
await ctx.State.Set("user", userData);
var user = await ctx.State.Get<UserData>("user"); // Returns actual value
Enter fullscreen mode Exit fullscreen mode

3. CLI Template System

The motia create -t csharp command needed to scaffold a complete project with:

  • Proper .csproj configuration
  • Example steps for all 4 types (api, event, cron, noop)
  • README with C#-specific instructions
  • Build system integration for cloud deployment

4. Multi-Language State Sharing

Making sure state set by TypeScript steps could be retrieved by C# steps (and vice versa) required careful JSON serialization. C#'s System.Text.Json needed to play nice with Node.js's serialization, especially for complex objects.

5. Process Management

Each C# step spawns a .NET process. Managing process lifecycle, handling crashes gracefully, and ensuring proper cleanup without memory leaks took some careful work with Node's process communication APIs.

Real-World Multi-Language Example

Imagine building an order processing system where each language does what it does best:

C# API Step (HTTP endpoint):

public static async Task<object> Handler(object req, dynamic ctx)
{
    await ctx.State.Set("order", orderData);
    await ctx.Emit(new { topic = "order.created", data = orderData });
    return new { status = 201, body = new { orderId = "123" } };
}
Enter fullscreen mode Exit fullscreen mode

Python Event Step (ML processing):

async def handler(input, context):
    order = await context.state.get("order")
    # Process with scikit-learn, tensorflow, etc.
    await context.emit({
        "topic": "order.analyzed",
        "data": {"risk_score": 0.95}
    })
Enter fullscreen mode Exit fullscreen mode

TypeScript Event Step (Business logic):

export const handler = async (input, { state, emit }) => {
  const order = await state.get("order");
  // Handle payment processing
  await emit({ topic: "order.completed", data: { orderId: order.id } });
};
Enter fullscreen mode Exit fullscreen mode

All three steps communicate automatically through Motia's event system with built-in observability!

Technical Deep Dive

Architecture Highlights

  1. Roslyn Scripting - Dynamic C# compilation for rapid development
  2. RPC over stdin/stdout - Efficient bidirectional communication
  3. Process isolation - Each C# step runs in its own process
  4. Automatic discovery - Just name files *.step.cs and Motia finds them

Performance Characteristics

  • Process spawn: ~200-300ms (first request)
  • Roslyn compilation: ~500-1000ms (dynamic)
  • RPC round-trip: ~1-5ms per operation
  • Memory overhead: ~40-60MB per process

Future Optimizations (Phase 7)

The implementation is feature-complete and well-tested, but here's what could improve performance further:

  • Process pooling - Reuse C# processes (50-70% faster cold starts)
  • Assembly caching - Reduce compilation time by 80%
  • NuGet package - Official Motia.Core package
  • Source generators - Reduce boilerplate with code generation
  • Middleware support - C# middleware chains for API steps
  • F# support - Functional programming on .NET

Testing

The implementation includes comprehensive testing:

  • Unit tests - Config parsing, execution, state operations
  • Integration tests - Full workflow validation
  • E2E tests - Template creation and deployment
  • Platform testing - Verified on macOS (compatible with Linux and Windows)

All tests passing, including the critical bidirectional state operations.

Trying It Out

If you want to test this before it's (potentially) merged:

# Clone my fork
git clone https://github.com/stimpy77/motia.git
cd motia
git checkout feat/csharp-dotnet-support

# Install dependencies
pnpm install

# Build
pnpm build

# Try the C# template
cd /tmp
npx ../motia/packages/snap/dist/index.js create -t csharp my-csharp-app
cd my-csharp-app
npx motia dev
Enter fullscreen mode Exit fullscreen mode

Or wait to see if the maintainers merge PR #769, then you can just use:

npx motia@latest create -t csharp my-csharp-app
Enter fullscreen mode Exit fullscreen mode

Prerequisites

Step Types Available

C# supports all 4 Motia step types:

Type Trigger Example Use Case
api HTTP Request REST endpoints, webhooks
event Topic subscription Background processing, queues
cron Schedule Daily reports, cleanup jobs
noop Manual External processes, UI helpers

Try It Yourself

Live Branch: feat/csharp-dotnet-support

Pull Request: #769 on MotiaDev/motia

The PR includes:

  • Complete implementation with bidirectional RPC
  • Comprehensive documentation
  • All tests passing
  • Phase 7 roadmap with future enhancements

Why This Matters

For C# Developers: Build modern backends without leaving the .NET ecosystem. Leverage your existing skills and libraries with strong typing and excellent IDE support.

For Motia Users: Use the best tool for each job - C# for APIs, Python for ML, TypeScript for business logic. Seamless cross-language communication with single deployment and unified observability.

For the Community: A reference implementation for multi-language frameworks with a working bidirectional RPC pattern and comprehensive testing.

What's Next?

The implementation is feature-complete with all tests passing. Phase 7 ideas for future optimization:

  1. NuGet package - Official Motia.Core for better IDE support
  2. Source generators - Auto-generate config from attributes
  3. Performance - Process pooling and assembly caching
  4. Middleware - C# middleware chains like TypeScript
  5. F# support - Functional programming on .NET

Feedback Welcome

If you try this out, I'd love to hear how it goes:

Resources


Built with: .NET 9 and TDD methodology.

Full disclosure (because honesty is fun): I built this entire implementation using Claude 4.5 Sonnet in Cursor. Every line of C# runner code, RPC protocol implementation, test fixtures, documentation, and yes, even this post explaining how I used AI to write it, was pair-programmed with AI. The irony is not lost on me. The TDD approach and architectural decisions were collaborative between human and AI, which is a fancy way of saying I described what I wanted and Claude wrote it while I just sat there barking at it when it broke.

If this bothers you, buckle up - this is how things are getting done now and it's only going to accelerate. 🤷

Special thanks to the Motia community and the existing Python/Ruby implementations that served as reference patterns.

What would you build with C# + Motia? Drop a comment below.


#csharp #dotnet #backend #opensource #developers #api #microservices #eventdriven

Top comments (0)