<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:discourse="http://discourse.org/rss/modules/discourse/">
<channel>
<title>Roslyn on Roque</title>
<link>https://j-roque.com/tags/roslyn/</link>
<description>Recent content in Roslyn on Roque</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<managingEditor>joao.roque.94@gmail.com (João Roque)</managingEditor>
<webMaster>joao.roque.94@gmail.com (João Roque)</webMaster>
<copyright>© 2026 João Roque</copyright>
<lastBuildDate>Tue, 22 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://j-roque.com/tags/roslyn/index.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Running C# Code in Connect IoT</title>
<link>https://j-roque.com/posts/20260922-csharp-codetask/</link>
<pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate>
<author>joao.roque.94@gmail.com (João Roque)</author>
<guid>https://j-roque.com/posts/20260922-csharp-codetask/</guid>
<description>Why a backend developer never has to leave C# to go from a custom service, to a Business Rule, to a low-code Connect IoT task.</description>
<content:encoded>&lt;p&gt;&lt;img src="https://j-roque.com/posts/20260922-csharp-codetask/featured.png" alt="Running C# Code in Connect IoT" /&gt;&lt;/p&gt;&lt;p&gt;Some months ago we showcased how we could have python as the main tool for building tasks &lt;a
href="https://j-roque.com/posts/20260406-python-codetask/"
target="_blank"
&gt;Python Code task&lt;/a&gt;. This made sense as the people who would interact with &lt;a
href="https://www.criticalmanufacturing.com/mes-for-industry-4-0/the-data-platform-for-manufacturers/"
target="_blank"
&gt;Data platform&lt;/a&gt; and analytics were data engineers and python reigns supreme.&lt;/p&gt;
&lt;p&gt;Hand a backend developer that same python or typescript sandbox and they will feel out of their comfort zone. In this blog post we showcase how we can design a solution that allows for C# native code tasks.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All the artifacts showed here can be downloaded and used at &lt;a
href="https://github.com/criticalmanufacturing/mes-common-library/tree/11.3/dev"
target="_blank"
&gt;MES Common Library&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 class="relative group"&gt;Overview
&lt;/h2&gt;
&lt;p&gt;For python, Pyodide running real CPython in a Connect IoT flow was our main leverage for making this work. This was aimed squarely at data engineers who live in &lt;code&gt;pandas&lt;/code&gt; and don&amp;rsquo;t want to leave it just to touch a shop floor. This post is about &lt;code&gt;c-sharp-roslyn-code&lt;/code&gt;, its C# counterpart, and it exists for a different reason than &amp;ldquo;give C# people the same toy.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The interesting part isn&amp;rsquo;t that it runs C#. It&amp;rsquo;s that the C# it runs is the &lt;em&gt;same&lt;/em&gt; C# a Critical Manufacturing backend developer already writes twice elsewhere: once in a custom .NET service that calls the MES directly, once in a Business Rule&amp;rsquo;s &lt;a
href="https://j-roque.com/posts/20260724-howdodeeswork/"
target="_blank"
&gt;DEE&lt;/a&gt; Action Code also as a low code DEE. This task is the forth surface, and the goal of this post is that moving between all three should cost you nothing, same language, same LBO contract, same completion engine and same mental model.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Hello World"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/csharpcodetask.gif"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h2 class="relative group"&gt;Two Kinds of Comfortable
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt;, &lt;strong&gt;Typescript&lt;/strong&gt; and &lt;strong&gt;C#&lt;/strong&gt; are not competing for the same job, they&amp;rsquo;re each home turf for a different person.&lt;/p&gt;
&lt;p&gt;A data engineer building an analytics pipeline is comfortable with a REPL, doesn&amp;rsquo;t care whether a variable is typed, and wants the python libraries more than they want the compiler yelling at them before they hit run. Pyodide&amp;rsquo;s &lt;code&gt;self.lbos&lt;/code&gt; dynamic bridge fits that: point it at an LBO by name, get a dictionary back, move on.&lt;/p&gt;
&lt;p&gt;A backend developer building shop-floor logic has spent years in the opposite world. They want the compiler to fail &lt;em&gt;before&lt;/em&gt; deployment, not during a production run at 3am. They want to type &lt;code&gt;_framework.System.Call&amp;lt;&lt;/code&gt; and see the actual generated output type autocomplete, not discover a typo in a string three hops downstream. They already know exactly what &lt;code&gt;Cmf.Navigo.BusinessOrchestration.ContainerManagement.InputObjects.EmptyContainerInput&lt;/code&gt; looks like, because they&amp;rsquo;ve constructed it before in Visual Studio, not in a browser sandbox.&lt;/p&gt;
&lt;p&gt;Building one code task and asking both audiences to use it would have satisfied neither. So there are three, each fitting a different target audience.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Four Surfaces, One Language
&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the part that actually matters for a backend developer&amp;rsquo;s day-to-day: writing C# against the MES was never new. What was missing was doing it &lt;em&gt;inside a Connect IoT flow&lt;/em&gt; without dropping down to a different toolchain.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;Surface&lt;/th&gt;
&lt;th style="text-align: left"&gt;Where the code runs&lt;/th&gt;
&lt;th style="text-align: left"&gt;Compiled&lt;/th&gt;
&lt;th style="text-align: left"&gt;Audience&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Custom backend service&lt;/td&gt;
&lt;td style="text-align: left"&gt;Your own .NET api extension of the &lt;a
href="https://developer.criticalmanufacturing.com/explore/guides/customizations/business/createservices/?h=service"
target="_blank"
&gt;MES API&lt;/a&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;Ahead of time, in Visual Studio&lt;/td&gt;
&lt;td style="text-align: left"&gt;Backend developer creating new APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;DEE Action Code (Business Rule)&lt;/td&gt;
&lt;td style="text-align: left"&gt;Inside the MES process itself&lt;/td&gt;
&lt;td style="text-align: left"&gt;Server-side, the moment you press save&lt;/td&gt;
&lt;td style="text-align: left"&gt;Backend or functional developer extending an out-of-the-box process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Business Workflows&lt;/td&gt;
&lt;td style="text-align: left"&gt;Inside the MES process itself&lt;/td&gt;
&lt;td style="text-align: left"&gt;Server-side, the moment you press save&lt;/td&gt;
&lt;td style="text-align: left"&gt;Citizen developers, similar to a DEE but with a low code designer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;C# Roslyn Code task&lt;/td&gt;
&lt;td style="text-align: left"&gt;The Connect IoT controller, in a child &lt;code&gt;dotnet&lt;/code&gt; process&lt;/td&gt;
&lt;td style="text-align: left"&gt;Roslyn, in-browser, when you save the flow&lt;/td&gt;
&lt;td style="text-align: left"&gt;Backend developer building shop-floor integration flows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Four different runtimes, four very different deployment stories, and the same language, the same &lt;code&gt;BaseInput&lt;/code&gt;/&lt;code&gt;BaseOutput&lt;/code&gt; LBO contract, and, as it turns out, the same completion engine philosophy.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Getting There Wasn&amp;rsquo;t the Obvious Path
&lt;/h2&gt;
&lt;p&gt;This isn&amp;rsquo;t the first attempt at a C# code task. The requirements were hard, it had to be able to have IDE like behavior in the frontend and be very performant in the Connect IoT runtime.&lt;/p&gt;
&lt;p&gt;We started with trying to go WASM (WebAssembly) using &lt;a
href="https://bootsharp.com"
target="_blank"
&gt;Bootsharp&lt;/a&gt; (the DotNetJS project) to compile a fixed C# runner project ahead-of-time to WASM. It worked, but every limitation traced back to the same root cause, Bootsharp is a build tool, not an interpreter, so there&amp;rsquo;s no &lt;code&gt;runPythonAsync&lt;/code&gt; style &amp;ldquo;just hand it source text&amp;rdquo; API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every controller host needed the .NET SDK &lt;em&gt;and&lt;/em&gt; the &lt;code&gt;wasm-tools&lt;/code&gt; workload installed, not just a runtime&lt;/li&gt;
&lt;li&gt;the first activation after any code change paid a real &lt;code&gt;dotnet publish&lt;/code&gt; cost, tens of seconds, more with NuGet packages&lt;/li&gt;
&lt;li&gt;no equivalent of Python&amp;rsquo;s dynamic &lt;code&gt;self.lbos&lt;/code&gt; bridge, and no retry-callback support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is a complaint on Bootsharp, it&amp;rsquo;s simply the wrong tool for &amp;ldquo;compile whatever the user just typed, right now&amp;rdquo;. With the Roslyn we split the problem in two: &lt;strong&gt;compile once&lt;/strong&gt;, &lt;strong&gt;execute many times&lt;/strong&gt;, on two different engines suited to each half.&lt;/p&gt;
&lt;h2 class="relative group"&gt;How It Actually Compiles and Runs
&lt;/h2&gt;
&lt;p&gt;This is not Roslyn &lt;em&gt;scripting&lt;/em&gt; (&lt;code&gt;CSharpScript.RunAsync&lt;/code&gt;), and it isn&amp;rsquo;t an interpreter loop of any kind. It&amp;rsquo;s the full Roslyn compiler API, producing a real assembly, split across two stages that run in two different places.&lt;/p&gt;
&lt;h3 class="relative group"&gt;Design time: compiling in the browser
&lt;/h3&gt;
&lt;p&gt;When you save the task, its settings component hands your source to a headless Blazor WebAssembly app bundled with the package rendered UI, it exists purely to run &lt;a
href="https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/"
target="_blank"
&gt;Roslyn&amp;rsquo;s compiler API&lt;/a&gt; client-side.&lt;/p&gt;
&lt;p&gt;It has a fixed catalog of the full .NET 8 BCL (Base Class Library) reference assemblies plus &lt;code&gt;RoslynCode.Contracts.dll&lt;/code&gt; (the &lt;code&gt;Framework&lt;/code&gt;/&lt;code&gt;Outputs&lt;/code&gt; API) and &lt;code&gt;Cmf.LightBusinessObjects.dll&lt;/code&gt;, all embedded as &lt;code&gt;EmbeddedResource&lt;/code&gt;s in the package at build time. There is no NuGet restore step here, whatever the browser can compile against is decided once, when the npm package itself is built, not per task. We reasoned that Python&amp;rsquo;s ecosystem assumes pip install at runtime, .NET&amp;rsquo;s doesn&amp;rsquo;t work that way even in normal development, so a fixed reference catalog isn&amp;rsquo;t the compromise for C# that it would be for Python.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The designer will not let you save a task whose C# doesn&amp;rsquo;t compile. &lt;code&gt;onBeforeSave&lt;/code&gt; runs the same &lt;code&gt;Compile&lt;/code&gt; call, and any error diagnostic throws before the settings are persisted. Compare that to the Python task, where a typo just fails the first time the flow activates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Compile error shown as an inline Monaco marker in the Code tab"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/codetask_error.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h3 class="relative group"&gt;Runtime: a warm .NET process on the controller
&lt;/h3&gt;
&lt;p&gt;The compiled assembly in base64, alongside the source and a &lt;code&gt;roslynCatalogVersion&lt;/code&gt; used to detect a stale compile against a newer catalog, is what actually gets persisted in the workflow. At runtime, the controller writes those bytes to disk and hands them to a bundled &lt;code&gt;RoslynCode.Host&lt;/code&gt; console app, launched as a genuine, separate OS process through &lt;code&gt;dotnet&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The goal is calling the Roslyn compiler API only once, in a browser tab, at save time. Then run plain &lt;code&gt;System.Reflection&lt;/code&gt; on every execution, in a collectible &lt;code&gt;AssemblyLoadContext&lt;/code&gt; that gets unloaded afterward, inside a real .NET process. The controller host only needs the .NET 8 &lt;strong&gt;runtime&lt;/strong&gt;, no SDK, no &lt;code&gt;wasm-tools&lt;/code&gt;, no &lt;code&gt;dotnet publish&lt;/code&gt; on the critical path, because compilation already happened somewhere else entirely and that cost was already paid.&lt;/p&gt;
&lt;p&gt;The two sides talk over a line-delimited JSON protocol on stdin/stdout. The controller sends one request; the host process can, before it answers, send any number of its own requests back, a log line, a data-store read, an MES system call, each correlated by a GUID and answered in turn.&lt;/p&gt;
&lt;p&gt;That bridge is what lets user C#, running in a separate OS process, still call &lt;code&gt;_framework.Logger.Info(...)&lt;/code&gt; or &lt;code&gt;_framework.System.Call(...)&lt;/code&gt; and have it land on the live controller, logging, the persisted data store, the message bus, the driver, and MES system calls all pump through this same request/response loop. This is the layer that allows us to leverage and stitch together the best of both worlds. The IoT layer interfaces with the C# execution.&lt;/p&gt;
&lt;p&gt;Unfortunately, this still took &lt;em&gt;&amp;gt;100ms&lt;/em&gt;. This cost was mainly just dynamically booting up the dotnet process to execute.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Connect IoT is a high-performance, high-data-volume application, it may deal with receiving events in the milliseconds range.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The solution was building a warm pool of dotnet processes ready to execute, instead of having to pay a fresh &lt;code&gt;dotnet&lt;/code&gt; startup cost every time, &lt;code&gt;warmPoolEnabled&lt;/code&gt; / &lt;code&gt;warmPoolSize&lt;/code&gt;. In each task we may define that a task is using the warm pool and if so, how many elements in that pool will be available. When performance really matters, the user can tweak this to extract the most performance.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Pool of warm RoslynCode.Host processes handling concurrent task activations"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/csharpcodetask_performance.gif"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Looking at some performance comparison data:&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Performance Chart"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/chart-e2e.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Roslyn Comparison"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/chart-roslyn.png"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The charts show how we are still sacrificing some performance against native typescript, but it&amp;rsquo;s much lower that the non warmed up version and at these time ranges it may be insignificant.&lt;/p&gt;
&lt;h2 class="relative group"&gt;The Template
&lt;/h2&gt;
&lt;p&gt;Same shape as the Typescript or Python task&amp;rsquo;s &lt;code&gt;class Code&lt;/code&gt;, translated into what a C# developer expects a &lt;code&gt;Code&lt;/code&gt; class to look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System.Text.Json.Nodes;
using System.Threading.Tasks;
using RoslynCode.Contracts;
public sealed class Code
{
    private readonly Framework _framework;
public Code(Framework framework)
    {
        _framework = framework;
    }
public async Task&amp;lt;JsonObject?&amp;gt; Main(JsonObject inputs, Outputs outputs)
    {
        // Add code here;
// Emit output during execution: outputs.Emit(&amp;#34;output1&amp;#34;, JsonValue.Create(value));
        // return new JsonObject { [&amp;#34;doubled&amp;#34;] = value * 2 };
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 class="relative group"&gt;Calling the MES Like You Always Do
&lt;/h2&gt;
&lt;p&gt;This is the actual &amp;ldquo;seamless&amp;rdquo; part, not just an architectural echo of DEE. &lt;code&gt;Framework.System.Call&amp;lt;TOutput&amp;gt;(BaseInput)&lt;/code&gt; is documented, in the contract source itself, as a direct mirror of the platform&amp;rsquo;s own JS-side call:&lt;/p&gt;
&lt;p&gt;This means calling an MES service is as simple as declaring the input object and calling the service.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s grab one of the simples API calls in the system, &lt;strong&gt;GetApplicationBootInformation&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;In the typescript code task we would do:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;    public async main(inputs: any, outputs: any): Promise&amp;lt;any&amp;gt; {
        const output = await this.framework.system.call(new this.framework.LBOS.Cmf.Foundation.BusinessOrchestration.ApplicationSettingManagement.InputObjects.GetApplicationBootInformationInput()) as LBOS.Cmf.Foundation.BusinessOrchestration.ApplicationSettingManagement.OutputObjects.GetApplicationBootInformationOutput;
outputs.result.emit(output.ApplicationInformation.ApplicationName);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In C# we would do:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;    public async Task&amp;lt;JsonObject?&amp;gt; Main(JsonObject inputs, Outputs outputs)
    {
        var output = await this._framework.System.Call&amp;lt;Cmf.Foundation.BusinessOrchestration.ApplicationSettingManagement.OutputObjects.GetApplicationBootInformationOutput&amp;gt;(new Cmf.Foundation.BusinessOrchestration.ApplicationSettingManagement.InputObjects.GetApplicationBootInformationInput());
outputs.Emit(&amp;#34;result&amp;#34;, output?.ApplicationInformation.ApplicationName);
        return null; 
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is first class support for api calls in our C# task.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure&gt;&lt;img
class="my-0 rounded-md"
loading="lazy"
decoding="async"
fetchpriority="low"
alt="Calling an MES Service"
src="https://image.j-roque.com/posts/20260922-csharp-codetask/csharpcodetask_servicecall.gif"
&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h2 class="relative group"&gt;Python vs. C# vs. Typescript, Side by Side
&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;Python Code task&lt;/th&gt;
&lt;th style="text-align: left"&gt;C# Roslyn Code task&lt;/th&gt;
&lt;th style="text-align: left"&gt;Typescript Code task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Audience&lt;/td&gt;
&lt;td style="text-align: left"&gt;Data engineers, analytics work&lt;/td&gt;
&lt;td style="text-align: left"&gt;Backend / .NET developers&lt;/td&gt;
&lt;td style="text-align: left"&gt;Backend / Typescript developers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Execution engine&lt;/td&gt;
&lt;td style="text-align: left"&gt;Pyodide, CPython compiled to WASM, interpreted in-process&lt;/td&gt;
&lt;td style="text-align: left"&gt;Real assembly, compiled ahead of time, executed by reflection in a separate &lt;code&gt;dotnet&lt;/code&gt; process&lt;/td&gt;
&lt;td style="text-align: left"&gt;Transpiled to javascript code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Compile-time safety&lt;/td&gt;
&lt;td style="text-align: left"&gt;None, errors surface at run time&lt;/td&gt;
&lt;td style="text-align: left"&gt;Full, the designer refuses to save code that doesn&amp;rsquo;t compile&lt;/td&gt;
&lt;td style="text-align: left"&gt;Full, the designer refuses to save code that doesn&amp;rsquo;t compile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;IntelliSense&lt;/td&gt;
&lt;td style="text-align: left"&gt;A hand-written completion provider&lt;/td&gt;
&lt;td style="text-align: left"&gt;The real Roslyn &lt;code&gt;CompletionService&lt;/code&gt;, same architecture as DEE&amp;rsquo;s&lt;/td&gt;
&lt;td style="text-align: left"&gt;Monaco enabled intellisense.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Library access&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;micropip&lt;/code&gt;, arbitrary PyPI packages, at runtime&lt;/td&gt;
&lt;td style="text-align: left"&gt;A fixed .NET reference catalog, baked in at package build time&lt;/td&gt;
&lt;td style="text-align: left"&gt;A fixed typescript reference catalog, baked in at package build time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;LBO access&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;self.lbos&lt;/code&gt;, dynamic, reflection-like, untyped&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;Framework.System.Call&amp;lt;TOutput&amp;gt;(BaseInput)&lt;/code&gt;, statically typed, same contract as a Business Rule&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;this._framework.system.Call&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Host prerequisite&lt;/td&gt;
&lt;td style="text-align: left"&gt;None beyond &lt;code&gt;npm install&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;.NET 8 runtime on the controller host&lt;/td&gt;
&lt;td style="text-align: left"&gt;None beyond &lt;code&gt;npm install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Neither of these is the &amp;ldquo;better&amp;rdquo; task, they&amp;rsquo;re built for opposite instincts. The Python task trusts the interpreter and rewards you for it with &lt;code&gt;import pandas&lt;/code&gt; and zero setup. The C# and typescript tasks trusts the compiler and rewards you for it with a save button that catches your mistakes before a machine ever sees them.&lt;/p&gt;
&lt;h2 class="relative group"&gt;Final Thoughts
&lt;/h2&gt;
&lt;p&gt;The interesting design decision here wasn&amp;rsquo;t &amp;ldquo;let people write C# in a flow.&amp;rdquo; It was noticing that C# in a flow is worthless to a backend developer unless it behaves like the C# they already write everywhere else in the platform, same LBO contract, same completion engine, same &amp;ldquo;here&amp;rsquo;s your input, return your output&amp;rdquo; shape as a Business Rule. Get that right, and moving from a bespoke integration service, to a DEE Action Code snippet, to a shop-floor flow stops being three skills and becomes one skill applied in three places.&lt;/p&gt;
&lt;p&gt;Data engineers get Pyodide and never have to see a compiler. Backend developers get Roslyn and never have to leave one. That&amp;rsquo;s the actual win, not that Connect IoT can run two languages, but that it stopped forcing either audience to be uncomfortable in the other&amp;rsquo;s.&lt;/p&gt;
</content:encoded>
<enclosure url="https://j-roque.com/posts/20260922-csharp-codetask/featured.png" type="image/png" length="0"/>
<media:content url="https://j-roque.com/posts/20260922-csharp-codetask/featured.png" type="image/png" medium="image"/>
</item>
</channel>
</rss>