Three experiments to find out what actually runs inside a Power Automate Custom Connector code-behin

The custom connector code-behind documentation on Microsoft Learn is clear: .NET Standard 2.0, 24 supported namespaces, one script file, no external libraries. Those constraints are real. They’re enforced at compile time by Roslyn — Microsoft’s open-source C# compiler that runs your code through a validation step before saving it to the connector.

What the documentation doesn’t say is what the runtime looks like underneath those constraints. The two things are not the same. What Roslyn accepts at compile time and what the process actually has loaded at runtime can be — and as these experiments show, are — very different.

🔷 This is not a Custom Connector tutorial

If you’re looking for a step-by-step build guide, the regex connector post covers that in full. This post is for people who already know what a Custom Connector is and want to understand what’s actually running underneath the code-behind — the host, the runtime version, which assemblies are in the process, and what becomes accessible if you go looking with System.Reflection.

Three experiments. Each one raises the bar. The first asks what’s already there. The second loads something that wasn’t. The third reaches a namespace Microsoft doesn’t even acknowledge exists in this context.

⚠️ Everything described here is undocumented, unsupported, and the code-behind feature has carried a “Preview” label since roughly 2020. This is a lab exercise — not a production pattern. The final section explains why.


What Microsoft Documents

The Write code in a custom connector page on Microsoft Learn states:

The currently supported .Net version is .NET Standard 2.0.

Microsoft Learn — Write code in a custom connector

The page also lists the only namespaces Roslyn will accept when you save connector code. This is the complete list — 26 entries. Reference anything outside it and the save fails with a compile error:

// Complete list of supported namespaces — Custom Connector code-behind
// Source: learn.microsoft.com/en-us/connectors/custom-connectors/write-code

using System;                          // Core types
using System.Collections;             // Non-generic collections
using System.Collections.Generic;     // Generic collections
using System.Diagnostics;             // Stopwatch, Debug
using System.IO;                      // File, Stream, Path
using System.IO.Compression;          // GZip, Deflate
using System.Linq;                    // LINQ queries
using System.Net;                     // HttpStatusCode, WebClient
using System.Net.Http;                // HttpClient, HttpRequestMessage
using System.Net.Http.Headers;        // HTTP headers
using System.Net.Security;            // SslStream
using System.Security.Authentication; // SslProtocols
using System.Security.Cryptography;   // AES, SHA, HMAC, RSA
using System.Text;                    // Encoding, StringBuilder
using System.Text.RegularExpressions; // Regex, Match
using System.Threading;               // Thread, CancellationToken
using System.Threading.Tasks;         // Task, async/await
using System.Web;                     // HttpUtility (URL/HTML encoding)
using System.Xml;                     // XmlDocument, XmlReader
using System.Xml.Linq;                // XDocument, XElement
using System.Drawing;                 // Bitmap, Graphics, Color (GDI+)
using System.Drawing.Drawing2D;       // GraphicsPath, Matrix
using System.Drawing.Imaging;         // ImageFormat, Encoder
using Microsoft.Extensions.Logging;   // ILogger
using Newtonsoft.Json;                // JsonConvert, JsonSerializer
using Newtonsoft.Json.Linq;           // JObject, JArray, JToken

A few things worth noting before the experiments:

  • System.Drawing is the most underused entry on this list. Full GDI+ support means you can generate images, draw text, resize bitmaps, or read EXIF metadata directly inside a connector operation — no external library needed. Almost no tutorial ever mentions it.
  • System.Security.Cryptography covers AES, SHA-256, HMAC, RSA — enough for signing, hashing, and encrypting payloads without any third-party crypto library.
  • System.Web.HttpUtility handles URL and HTML encoding/decoding — a small but useful utility that saves writing your own encoding logic.
  • Missing from this list: System.Reflection, System.Globalization, System.Text.Json, any Azure SDK, MSAL. No external NuGet packages. No multi-file scripts.

Here is what each namespace group actually gives you:

System Core — the foundation types present in every connector operation
I/O and Compression — in-memory streams and payload compression
Network and HTTP — the full HTTP client stack for outbound connector calls
Security and Cryptography — hash, encrypt, sign without any third-party library
Text and Pattern Matching — encoding, full .NET regex, and URL utilities
XML — classic DOM and modern LINQ-based XML processing
Drawing GDI+ — image generation and manipulation, the most underused namespace group in the documented list
Microsoft.Extensions.Logging and Newtonsoft.Json — logging and dynamic JSON manipulation

What the page doesn’t describe is what the runtime actually has loaded at the moment your ExecuteAsync() runs. That’s a different question — and the answer is what the three experiments below reveal.


The Connector Setup

All three experiments use the same connector: Reflection Lab Connector, built inside a Power Platform Solution in a DEV environment (US region). A single operation — ReflectionTest — accepts one text input and returns a JSON object whose fields grow with each experiment.

The Swagger definition for the operation. Note host: www.microsoft.com — a placeholder required by the framework even though no call ever reaches it. The code-behind intercepts every request before it would be forwarded anywhere:

swagger: "2.0"
info:
  title: Reflection Lab Connector
  description: Lab experiment — reflection and dynamic assembly loading in Power Automate code-behind.
  version: "1.0"
host: www.microsoft.com
basePath: /
schemes:
  - https
paths:
  /reflect:
    post:
      summary: Reflection Test
      operationId: ReflectionTest
      x-ms-visibility: important
      parameters:
        - in: body
          name: body
          required: true
          schema:
            type: object
            required: [input]
            properties:
              input:
                type: string
                x-ms-summary: Input
      responses:
        "200":
          description: Result
          schema:
            type: object
            properties:
              input: { type: string }
              result: { type: string }
              assembliesLoaded: { type: string }

Test 1 — What’s Already in the Room?

The first experiment doesn’t load anything external. It just looks at what’s already there.

AppDomain — the .NET concept for the current execution boundary — holds all the assemblies loaded into a running process. AppDomain.CurrentDomain.GetAssemblies() returns everything present at the moment your code runs. Not what Microsoft says is available. What’s actually there.

The test also does two more things — and the choice of which namespaces to use here is deliberate:

  • HMAC-SHA256 via MethodInfo.Invoke()System.Security.Cryptography is in Microsoft’s documented list, so we know it’s supposed to work. Using it via reflection instead of a direct call validates that MethodInfo.Invoke() itself functions correctly in this runtime. If this step fails, reflection is broken at the invocation level and there’s no point going further.
  • System.Xml from the GAC by strong nameSystem.Xml is also in the documented list, which means it’s expected to be available. Attempting to load it via Assembly.Load(strong name) from the Global Assembly Cache (the machine-wide repository where .NET stores shared libraries) tests whether the runtime has a populated GAC at all. If a documented namespace loads from the GAC, undocumented ones might too.

These two steps are the control group. They use known-good namespaces to establish that the reflection mechanics work before applying those same mechanics to namespaces Microsoft has never documented.

The C# Code — Test 1

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

public class Script : ScriptBase
{
    public override async Task ExecuteAsync()
    {
        if (this.Context.OperationId == "ReflectionTest")
            return await this.HandleReflectionTest().ConfigureAwait(false);

        var error = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
        error.Content = CreateJsonContent($"Unknown operation: {this.Context.OperationId}");
        return error;
    }
    private async Task HandleReflectionTest()
    {
        var body = await this.Context.Request.Content
            .ReadAsStringAsync().ConfigureAwait(false);
        var json = JObject.Parse(body);
        var input = (string)json["input"] ?? string.Empty;

        // Step 1: enumerate all assemblies loaded in the current AppDomain
        // This is the map of everything available at runtime — not just the documented list
        var loadedAssemblies = AppDomain.CurrentDomain
            .GetAssemblies()
            .Select(a => a.GetName().Name)
            .OrderBy(n => n)
            .ToList();

        // Step 2: invoke HMAC-SHA256 via reflection — CONTROL GROUP
        // System.Security.Cryptography IS in the documented list.
        // We invoke it via MethodInfo.Invoke() instead of calling it directly
        // to validate that reflection invocation itself works in this runtime.
        // If this step fails, the mechanism is broken before we test anything undocumented.
        string reflectionResult, methodDescription;
        try
        {
            var cryptoAssembly = typeof(HMACSHA256).Assembly;
            var hmacType = cryptoAssembly.GetType("System.Security.Cryptography.HMACSHA256");
            var key = Encoding.UTF8.GetBytes("lab-secret-key");
            var hmac = (HMACSHA256)Activator.CreateInstance(hmacType, new object[] { key });
            var computeHashMethod = hmacType.GetMethod("ComputeHash", new[] { typeof(byte[]) });
            var hashBytes = (byte[])computeHashMethod.Invoke(hmac, new object[] {
                Encoding.UTF8.GetBytes(input) });
            reflectionResult = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
            methodDescription = $"HMAC-SHA256 via reflection on {hmacType.FullName}";
        }
        catch (Exception ex) { reflectionResult = $"Error: {ex.Message}"; methodDescription = "failed"; }

        // Step 3: load System.Xml from the GAC by strong name — CONTROL GROUP
        // System.Xml IS in the documented list — we expect it to be available.
        // Loading it via Assembly.Load(strong name) tests whether the GAC is accessible.
        // If a documented namespace loads from the GAC, undocumented assemblies may too.
        string diskLoadResult;
        try
        {
            var xmlAssembly = Assembly.Load(
                "System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
            var xmlDocType = xmlAssembly.GetType("System.Xml.XmlDocument");
            diskLoadResult = xmlDocType != null
                ? $"Loaded System.Xml from GAC — found XmlDocument at {xmlDocType.FullName}"
                : "Loaded System.Xml but XmlDocument not found";
        }
        catch (Exception ex) { diskLoadResult = $"GAC load failed: {ex.Message}"; }

        var result = new JObject {
            ["input"] = input, ["result"] = reflectionResult,
            ["method"] = methodDescription, ["diskLoadAttempt"] = diskLoadResult,
            ["assembliesLoaded"] = string.Join(", ", loadedAssemblies),
        };
        var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        response.Content = CreateJsonContent(result.ToString());
        return response;
    }
}

💡 Note on using System.Reflection

That namespace is not in Microsoft’s documented supported list. It compiled and ran without error. That observation matters — we come back to it in the conclusions.

System.Reflection — not in the documented list, yet Roslyn accepted it. These are the six types used across the three experiments.

How to Run Test 1

  1. Open the connector in the maker portal → tab 4. Code
  2. Toggle Code Enabled → select ReflectionTest in the Operations dropdown
  3. Replace the default Hello World code with the script above
  4. Click Create connector — wait 20–30 seconds for the save to complete
  5. Tab 5. Test+ New connection → fill input with any text → Test operation
  6. Click the Response tab

What Came Back

assembliesLoaded: Microsoft.Azure.WebJobs.Script, Microsoft.AspNetCore.*,
Microsoft.CodeAnalysis.CSharp.Scripting, Microsoft.PowerPlatform.Connectors.CustomCode.CSharp,
System.Text.Json, Microsoft.Identity.Client, Azure.Core, Azure.Storage.Blobs,
Azure.Data.Tables, System.Reflection.Emit, Autofac, ScriptHost (×4),
System.Security.Cryptography.Algorithms v6.0.0.0, ...

result: f68618f7cf3308c639eae3ca9189c5590f4fb66fc40d549a4f69e2950fc2f016
method: HMAC-SHA256 via reflection on System.Security.Cryptography.HMACSHA256
diskLoadAttempt: Loaded System.Xml from GAC — found XmlDocument at System.Xml.XmlDocument

⚡ The runtime is not a .NET Standard 2.0 sandbox. It’s Azure Functions on .NET 6.

Key assemblies found in the AppDomain:

AssemblyWhat it reveals
Microsoft.Azure.WebJobs.ScriptThe host is Azure Functions — WebJobs Script host specifically
Microsoft.AspNetCore.*Full ASP.NET Core stack underneath
Microsoft.CodeAnalysis.CSharp.ScriptingRoslyn is compiling your code at runtime. The namespace restriction is enforced here — at compilation — not by the runtime itself
System.Text.JsonThe modern .NET JSON parser — not in Microsoft’s documented list
Microsoft.Identity.ClientMSAL — also not in the documented list
System.Reflection.EmitIL code generation — you can generate code dynamically at runtime
System.Security.Cryptography.Algorithms v6.0.0.0.NET 6 confirmed — not .NET Standard 2.0
Key assemblies found in AppDomain — Test 1

HMAC-SHA256 via MethodInfo.Invoke() ✅. System.Xml from the GAC ✅. The .NET Standard 2.0 label describes the compilation target. Not the runtime.

The two lists: what Roslyn accepts at compile time (left) vs what the AppDomain actually contains at runtime (right). Note the SURPRISE items — none are in the documented list.

Now We Raise the Bar — Test 2: Loading an External DLL

The first test worked with assemblies already in the process. Test 2 loads something that wasn’t there at all.

The approach: write a small .NET Standard 2.0 class library in Visual Studio, compile it, convert the DLL to a Base64 string, embed that string as a constant in the connector code, and load it at runtime via Assembly.Load(byte[]).

Step 1 — Create the Class Library in Visual Studio

  1. File → New → Project
  2. Search for Class Library → select Class Library (.NET Standard)
  3. Project name: MyUtilLib
  4. Framework: .NET Standard 2.0
  5. Click Create

Rename the default Class1.cs to TextUtils.cs (right-click in Solution Explorer → Rename → Yes to rename the class too). Replace the content with:

using System;
using System.Text.RegularExpressions;

namespace MyUtilLib
{
    public class TextUtils
    {
        // Removes all non-alphanumeric characters except spaces and hyphens
        public static string Sanitize(string input)
        {
            if (string.IsNullOrEmpty(input)) return string.Empty;
            return Regex.Replace(input, @"[^a-zA-Z0-9\s\-]", string.Empty).Trim();
        }

        // Extracts all numbers from a string
        public static string[] ExtractNumbers(string input)
        {
            if (string.IsNullOrEmpty(input)) return Array.Empty();
            var matches = Regex.Matches(input, @"\d+");
            var result = new string[matches.Count];
            for (int i = 0; i < matches.Count; i++)
                result[i] = matches[i].Value;
            return result;
        }
    }
}
TextUtils.cs in Visual Studio — one class, two methods, one documented namespace: System.Text.RegularExpressions

Step 2 — Build in Release Mode

Change the build configuration dropdown from Debug to Release, then Build → Rebuild Solution.

⚠️ Use Release — not Debug

A Debug build includes PDB symbols and metadata that can cause BadImageFormatException when loaded in a different runtime context. The DLL from a Release build is a clean binary.

bin/Release/netstandard2.0 after build and conversion. The same three files appear every time you rebuild — only MyUtilLib.b64.txt changes content when you add classes.

Step 3 — Convert the DLL to Base64

Open View → Terminal in Visual Studio (or press Ctrl+`). Run these commands one at a time, using your full path:

$bytes = [System.IO.File]::ReadAllBytes("C:\...\MyUtilLib\bin\Release\netstandard2.0\MyUtilLib.dll")
$base64 = [System.Convert]::ToBase64String($bytes)
[System.IO.File]::WriteAllText("C:\...\MyUtilLib\bin\Release\netstandard2.0\MyUtilLib.b64.txt", $base64, [System.Text.Encoding]::ASCII)
Write-Host "Length: $($base64.Length)"

Use [System.IO.File]::WriteAllText() — not Out-File. Out-File appends a trailing newline character that is invisible but causes error CS1010: Newline in constant when Roslyn tries to compile the string literal inside the connector.

💡 If your path contains spaces

Common with OneDrive folders. Use the full absolute path directly in each command — the .\ relative path notation does not resolve reliably in Visual Studio’s PowerShell terminal when the working path contains spaces.

Open MyUtilLib.b64.txt — the file starts with TVqQ, which is the standard PE/DLL header in Base64. That confirms the encoding worked correctly.

the Base64-encoded DLL ready to embed in the connector code. The string starts with TVqQ, the standard PE/DLL header in Base64.

Step 4 — Write the Connector Code

The connector loads the DLL from the embedded Base64 bytes and invokes TextUtils.Sanitize() via reflection:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Script : ScriptBase
{
    // ↓ Paste the full content of MyUtilLib.b64.txt here (one line, starts with TVqQ)
    private const string MyUtilLibBase64 = "here b64";

    private static Assembly LoadMyLib()
    {
        // Decode Base64 → raw bytes → load DLL into the current AppDomain
        byte[] dllBytes = Convert.FromBase64String(MyUtilLibBase64);
        return Assembly.Load(dllBytes);
    }

    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        if (this.Context.OperationId == "ReflectionTest")
            return await this.HandleReflectionTest().ConfigureAwait(false);

        var error = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
        error.Content = CreateJsonContent($"Unknown operation: {this.Context.OperationId}");
        return error;
    }

    private async Task<HttpResponseMessage> HandleReflectionTest()
    {
        var body = await this.Context.Request.Content
            .ReadAsStringAsync().ConfigureAwait(false);
        var json = JObject.Parse(body);
        var input = (string)json["input"] ?? string.Empty;

        string result;
        try
        {
            var assembly = LoadMyLib();

            // Locate the class and method by name — no compile-time reference needed
            var textUtilsType = assembly.GetType("MyUtilLib.TextUtils");
            var sanitizeMethod = textUtilsType.GetMethod(
                "Sanitize", BindingFlags.Public | BindingFlags.Static);

            // null = static method (no instance), followed by the argument array
            result = (string)sanitizeMethod.Invoke(null, new object[] { input });
        }
        catch (Exception ex)
        {
            result = $"Error: {ex.GetType().Name} — {ex.Message}";
        }

        var output = new JObject
        {
            ["input"] = input,
            ["result"] = result,
        };

        var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        response.Content = CreateJsonContent(output.ToString());
        return response;
    }
}

Result ✅

Input:  "Hello World! 123 test@email.com #special$chars"
Output: "Hello World 123 testemailcom specialchars"

Assembly.Load(byte[]) worked. The DLL loaded, the class was found, the method was invoked. All special characters (!, @, ., #, $) removed correctly by TextUtils.Sanitize().

🧪 Now try it with your own logic

The DLL embedded here has one class with two methods. But there is no limit on what you put inside it — as long as it targets .NET Standard 2.0 and the compiled DLL fits within the 1 MB connector code size limit. Any logic you can write in a class library can be loaded and invoked from a connector operation. The next test does exactly that with a second class — using a namespace that has no parent in the documented list.

Test 2 — connector response confirming Assembly.Load(byte[]) worked: TextUtils.Sanitize() invoked from an externally loaded DLL

And Again — Test 3: Two Classes, One Undocumented Namespace

Tests 1 and 2 used namespaces that are in Microsoft’s documented list. Test 3 targets System.Globalization — a namespace with no parent anywhere in that list.

The experiment adds a second class to MyUtilLib that uses System.Globalization and loads both classes from the same DLL.

Add CultureUtils.cs to the Project

In Visual Studio, right-click MyUtilLib in Solution Explorer → Add → New Item → Class → name it CultureUtils.cs:

using System;
using System.Globalization;
using System.Linq;

namespace MyUtilLib
{
    public class CultureUtils
    {
        // System.Globalization has NO parent namespace in Microsoft's documented list
        // Returns the culture configured on the Azure Functions host process
        public static string GetHostCulture()
        {
            var current = CultureInfo.CurrentCulture;
            var ui = CultureInfo.CurrentUICulture;
            return $"Culture: {current.Name} ({current.EnglishName}) | " +
                   $"UI: {ui.Name} ({ui.EnglishName})";
        }
    }
}
CultureUtils.cs added to the project — Solution Explorer now shows two classes. System.Globalization is not in Microsoft’s documented list and has no parent namespace in the list either — but it compiles and runs without error.

Rebuild, Reconvert, Re-embed

Same process as before: Release build → PowerShell commands → new Base64 string → update connector code.

Write the Connector Code

The connector loads the DLL from the embedded Base64 bytes and invokes both classes via reflection TextUtils.Sanitize() as the control group, and , and CultureUtils.GetHostCulture() to test a namespace with no parent in the documented list.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Script : ScriptBase
{
    // ↓ Paste the full content of MyUtilLib.b64.txt here (one line, starts with TVqQ)
    //   This DLL contains 2 classes: TextUtils + CultureUtils
    private const string MyUtilLibBase64 = "here b64";

    private static Assembly LoadMyLib()
    {
        // Decode Base64 → raw bytes → load DLL into the current AppDomain
        byte[] dllBytes = Convert.FromBase64String(MyUtilLibBase64);
        return Assembly.Load(dllBytes);
    }

    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        if (this.Context.OperationId == "ReflectionTest")
            return await this.HandleReflectionTest().ConfigureAwait(false);

        var error = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
        error.Content = CreateJsonContent($"Unknown operation: {this.Context.OperationId}");
        return error;
    }

    private async Task<HttpResponseMessage> HandleReflectionTest()
    {
        var body = await this.Context.Request.Content
            .ReadAsStringAsync().ConfigureAwait(false);
        var json = JObject.Parse(body);
        var input = (string)json["input"] ?? string.Empty;

        var assembly = LoadMyLib();
        var output = new JObject { ["input"] = input };

        // TextUtils.Sanitize — System.Text.RegularExpressions (documented)
        // CONTROL GROUP: validates MethodInfo.Invoke() before testing undocumented namespaces
        try
        {
            var t = assembly.GetType("MyUtilLib.TextUtils");
            var m = t.GetMethod("Sanitize", BindingFlags.Public | BindingFlags.Static);
            output["sanitized"] = (string)m.Invoke(null, new object[] { input });
        }
        catch (Exception ex) { output["sanitized"] = $"Error: {ex.Message}"; }

        // CultureUtils.GetHostCulture — System.Globalization (NOT in documented list)
        // No parent namespace in Microsoft's list — confirmed reachable via reflection
        try
        {
            var t = assembly.GetType("MyUtilLib.CultureUtils");
            var m = t.GetMethod("GetHostCulture", BindingFlags.Public | BindingFlags.Static);
            output["hostCulture"] = (string)m.Invoke(null, new object[0]);
        }
        catch (Exception ex) { output["hostCulture"] = $"Error: {ex.Message}"; }

        var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        response.Content = CreateJsonContent(output.ToString());
        return response;
    }
}

Result ✅

{
  "input": "test input string",
  "sanitized": "test input string",
  "hostCulture": "Culture: en-US (English (United States)) | UI: en-US (English (United States))"
}

System.Globalization is reachable via reflection. A namespace with no parent in the documented list, working correctly inside a connector operation.

The en-US result is worth noting by itself. The tenant running this experiment is based in Italy. The Azure Functions host reports en-US — the culture of the process is US English regardless of where the tenant is located. A small but concrete detail about what the runtime actually is.

🧪 What else can you reach?

System.Globalization has no parent in the documented list — yet it compiled and ran. That raises the obvious question: what other undocumented namespaces are reachable the same way? The assembliesLoaded field from Test 1 is your map. A few worth trying: System.Runtime.Serialization, System.ComponentModel, System.Numerics. Add a class to MyUtilLib, rebuild, regenerate the Base64, update the connector. The pattern is the same every time.

Test 3 — two classes from the same DLL, one using System.Globalization (not in documented list). The en-US culture confirms the Azure Functions host is US English regardless of tenant geography.

The Bonus Round — JWT Decoding

Three classes. The third one — JwtUtils — uses only System.Text and System.Convert, both documented. But it implements something Power Automate expressions cannot do natively: Base64Url decoding.

JWT tokens use Base64Url — a variant of Base64 that replaces + with - and / with _, and drops the padding = characters. Decoding one in a Power Automate Cloud flow without a connector requires:

  1. Two replace() expressions to normalize - and _ back to + and /
  2. A mod() calculation to determine padding
  3. A Condition block with three branches (pad 0, 1, or 2 = characters)
  4. base64ToString() to decode
  5. json() to parse

Add JwtUtils.cs to the Project

In Visual Studio, right-click MyUtilLib in Solution Explorer → Add → New Item → Class → name it JwtUtils.cs:

using System;
using System.Collections.Generic;
using System.Text;

namespace MyUtilLib
{
    public class JwtUtils
    {
        // Decodes a JWT token and returns all claims as JSON
        // Uses only System.Text and System.Convert — no external libraries
        // JWT format: header.payload.signature (Base64Url encoded)
        public static string DecodeToken(string token)
        {
            try
            {
                if (string.IsNullOrEmpty(token))
                    return "{\"error\": \"Token is empty\"}";

                // Remove "Bearer " prefix if present
                if (token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                    token = token.Substring(7);

                var parts = token.Split('.');
                if (parts.Length != 3)
                    return "{\"error\": \"Invalid JWT format — expected 3 parts\"}";

                // Decode header
                var header = DecodeBase64Url(parts[0]);

                // Decode payload (contains the claims)
                var payload = DecodeBase64Url(parts[1]);

                return "{\"header\": " + header + ", \"payload\": " + payload + "}";
            }
            catch (Exception ex)
            {
                return "{\"error\": \"" + ex.GetType().Name + ": " + ex.Message + "\"}";
            }
        }

        // Extracts a specific claim value from a JWT token
        public static string GetClaim(string token, string claimName)
        {
            try
            {
                if (string.IsNullOrEmpty(token))
                    return "Token is empty";

                if (token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                    token = token.Substring(7);

                var parts = token.Split('.');
                if (parts.Length != 3)
                    return "Invalid JWT format";

                var payload = DecodeBase64Url(parts[1]);

                // Simple claim extraction without JSON parser
                // Looks for "claimName":"value" or "claimName":value
                var searchKey = "\"" + claimName + "\":";
                var idx = payload.IndexOf(searchKey);
                if (idx < 0)
                    return $"Claim '{claimName}' not found";

                var valueStart = idx + searchKey.Length;
                while (valueStart < payload.Length && payload[valueStart] == ' ')
                    valueStart++;

                if (valueStart >= payload.Length)
                    return "Value not found";

                // String value
                if (payload[valueStart] == '"')
                {
                    var valueEnd = payload.IndexOf('"', valueStart + 1);
                    return valueStart + 1 < valueEnd
                        ? payload.Substring(valueStart + 1, valueEnd - valueStart - 1)
                        : string.Empty;
                }

                // Number or boolean value
                var end = payload.IndexOfAny(new[] { ',', '}', ']' }, valueStart);
                return end > valueStart
                    ? payload.Substring(valueStart, end - valueStart).Trim()
                    : payload.Substring(valueStart).Trim();
            }
            catch (Exception ex)
            {
                return $"Error: {ex.GetType().Name} — {ex.Message}";
            }
        }

        // Returns token expiry as readable datetime
        public static string GetExpiry(string token)
        {
            try
            {
                var expStr = GetClaim(token, "exp");
                if (expStr.StartsWith("Error") || expStr.StartsWith("Claim") || expStr.StartsWith("Invalid"))
                    return expStr;

                if (!long.TryParse(expStr, out var exp))
                    return $"Cannot parse exp value: {expStr}";

                var expiry = DateTimeOffset.FromUnixTimeSeconds(exp).UtcDateTime;
                var now = DateTime.UtcNow;
                var diff = expiry - now;

                var status = diff.TotalSeconds > 0
                    ? $"VALID — expires in {(int)diff.TotalMinutes} minutes"
                    : $"EXPIRED — {Math.Abs((int)diff.TotalMinutes)} minutes ago";

                return $"{expiry:yyyy-MM-dd HH:mm:ss} UTC | {status}";
            }
            catch (Exception ex)
            {
                return $"Error: {ex.GetType().Name} — {ex.Message}";
            }
        }

        // Base64Url decode — JWT uses Base64Url (replaces + with - and / with _)
        private static string DecodeBase64Url(string base64Url)
        {
            var base64 = base64Url
                .Replace('-', '+')
                .Replace('_', '/');

            // Pad to multiple of 4
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }

            var bytes = Convert.FromBase64String(base64);
            return Encoding.UTF8.GetString(bytes);
        }
    }
}
Test 4 — three classes from the same DLL, one decoding JWT natively. Base64Url is not in Power Automate expressions — the connector handles it in a single action.

Five steps for one claim. JwtUtils does all of it in a single invocation.

Rebuild, Reconvert, Re-embed

Same process as before: Release build → PowerShell commands → new Base64 string → update connector code.

We use the public test JWT from jwt.io as input — alg: HS256, name: Vincenzo in the payload, exp: 9999999999 (year 2286). Paste this in the input field: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlZpbmNlbnpvIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.GRzcqGBiVDegcL6Au6c-rhObvfsnX8FJi-PC-QFHPIIp

Write the Connector Code

The connector loads the DLL from the embedded Base64 bytes and invokes all three classes via reflection — TextUtils.Sanitize() as the control group, CultureUtils.GetHostCulture() to test an undocumented namespace, and JwtUtils.DecodeToken() to decode a JWT without any native Power Automate expression support:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Script : ScriptBase
{
    // ↓ DLL contains 3 classes: TextUtils + CultureUtils + JwtUtils
    private const string MyUtilLibBase64 = "here b64";

    private static Assembly LoadMyLib()
    {
        // Decode Base64 → raw bytes → load DLL into the current AppDomain
        byte[] dllBytes = Convert.FromBase64String(MyUtilLibBase64);
        return Assembly.Load(dllBytes);
    }

    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        if (this.Context.OperationId == "ReflectionTest")
            return await this.HandleReflectionTest().ConfigureAwait(false);

        var error = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
        error.Content = CreateJsonContent($"Unknown operation: {this.Context.OperationId}");
        return error;
    }

    private async Task<HttpResponseMessage> HandleReflectionTest()
    {
        var body = await this.Context.Request.Content
            .ReadAsStringAsync().ConfigureAwait(false);
        var json = JObject.Parse(body);
        var input = (string)json["input"] ?? string.Empty;

        var assembly = LoadMyLib();
        var output = new JObject { ["input"] = input };

        // TextUtils.Sanitize — System.Text.RegularExpressions (documented)
        // CONTROL GROUP: validates MethodInfo.Invoke() before testing undocumented namespaces
        try
        {
            var t = assembly.GetType("MyUtilLib.TextUtils");
            var m = t.GetMethod("Sanitize", BindingFlags.Public | BindingFlags.Static);
            output["sanitized"] = (string)m.Invoke(null, new object[] { input });
        }
        catch (Exception ex) { output["sanitized"] = $"Error: {ex.Message}"; }

        // CultureUtils.GetHostCulture — System.Globalization (NOT in documented list)
        // No parent namespace in Microsoft's list — confirmed reachable via reflection
        try
        {
            var t = assembly.GetType("MyUtilLib.CultureUtils");
            var m = t.GetMethod("GetHostCulture", BindingFlags.Public | BindingFlags.Static);
            output["hostCulture"] = (string)m.Invoke(null, new object[0]);
        }
        catch (Exception ex) { output["hostCulture"] = $"Error: {ex.Message}"; }

        // JwtUtils.DecodeToken — System.Text + System.Convert (documented)
        // Implements Base64Url decoding — not natively available in Power Automate expressions
        try
        {
            var t = assembly.GetType("MyUtilLib.JwtUtils");
            var m = t.GetMethod("DecodeToken", BindingFlags.Public | BindingFlags.Static);
            var decoded = (string)m.Invoke(null, new object[] { input });
            output["jwtDecoded"] = JToken.Parse(decoded);
        }
        catch (Exception ex) { output["jwtDecoded"] = $"Error: {ex.Message}"; }

        // JwtUtils.GetExpiry — returns human-readable expiry date and VALID/EXPIRED status
        try
        {
            var t = assembly.GetType("MyUtilLib.JwtUtils");
            var m = t.GetMethod("GetExpiry", BindingFlags.Public | BindingFlags.Static);
            output["jwtExpiry"] = (string)m.Invoke(null, new object[] { input });
        }
        catch (Exception ex) { output["jwtExpiry"] = $"Error: {ex.Message}"; }

        var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        response.Content = CreateJsonContent(output.ToString());
        return response;
    }
}

Result ✅

{
  "input": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlZpbmNlbnpvIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
  "sanitized": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlZpbmNlbnpvIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTl9SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c",
  "hostCulture": "Culture: en-US (English (United States)) | UI: en-US (English (United States))",
  "jwtDecoded": {
    "header": {
      "alg": "HS256",
      "typ": "JWT"
    },
    "payload": {
      "sub": "1234567890",
      "name": "Vincenzo",
      "iat": 1516239022,
      "exp": 9999999999
    }
  },
  "jwtExpiry": "2286-11-20 17:46:39 UTC | VALID — expires in 136922554 minutes"
}

The test token has exp: 9999999999 — Unix timestamp year 2286. The connector calculated that correctly. ✅

Bonus round — JWT decoded in a single connector action: header, full payload, and expiry status with human-readable date

Two Different Lists

This is the central finding. There are two separate things that people conflate when talking about what’s “available” in the connector runtime.

The documented namespace list is a Roslyn compile-time constraint, not a runtime sandbox.

— vsgueradev.com

List 1 — What Roslyn accepts at compile time

The documented namespace list. These are the only using statements Roslyn will accept when you save connector code. Reference anything else and the save fails.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
// 26 total — source: learn.microsoft.com/en-us/connectors/custom-connectors/write-code

List 2 — What’s actually in the AppDomain at runtime

From the assembliesLoaded field returned by Test 1. None of these are in List 1, but all are reachable via reflection:

  • System.Text.Json — the modern .NET JSON parser
  • Microsoft.Identity.Client — MSAL
  • Azure.Core, Azure.Data.Tables, Azure.Storage.Blobs — Azure SDK
  • System.Reflection.Emit — IL code generation
  • Microsoft.CodeAnalysis.CSharp.Scripting — Roslyn itself
  • Microsoft.Azure.WebJobs.Script — the Azure Functions host

The distinction that matters

LayerControllerEffect
CompilationRoslyn, enforced by documented listCannot write using System.Text.Json; — compile error
RuntimeAzure Functions host, no restriction foundSystem.Text.Json is in the AppDomain — reachable via reflection
External DLLAssembly.Load(byte[]) — confirmed workingAny .NET Standard 2.0 compatible DLL can be loaded from embedded bytes
Three layers — who controls what

One more observation: using System.Reflection; is not in the documented list. It compiled and ran without error across all three tests. The documented list appears to describe an intended support boundary, not a hard enforcement boundary at the Roslyn level.

Microsoft tells you which libraries you can name in your source code. They do not prevent you from loading other libraries at runtime via reflection. Whether that’s intentional or an oversight is not documented.


Why This Doesn’t Go to Production

Four reasons, in order of weight.

1. Governance is blind to it

Power Platform DLP policies — the tenant-level rules that control which connectors and data sources are allowed in a given environment — see a custom connector as a black box. They cannot inspect what assemblies are loaded at runtime. An admin who approves the connector has no visibility into what it’s actually executing.

2. It’s outside the documented contract

The connector terms of service describe the supported namespaces. Loading assemblies via reflection bypasses that contract silently — not by throwing an error, but by simply working in a way Microsoft hasn’t acknowledged.

3. Preview on Preview

The code-behind feature has carried a “Preview” label since roughly 2020. Building on an undocumented technique inside a Preview feature is two layers of instability stacked.

4. The failure mode is silent

If Microsoft changes the Azure Functions host version, adds an Assembly.Load restriction, or changes the AppDomain isolation model, the connector stops working with no warning and no useful error message. In production, that’s a broken business process that’s hard to diagnose and impossible to fix quickly.

For understanding what the runtime actually is — valuable. For anything a client depends on — keep it far away.


What Else Is in There? Your Turn.

These three experiments tested System.Globalization as the undocumented namespace. But the AppDomain contains far more. And the documented list has entries that nobody ever talks about in connector tutorials.

Two directions worth exploring:

Direction 1 — Undocumented namespaces already in the AppDomain

The assembliesLoaded field from Test 1 is your map. Every assembly listed there is reachable via reflection. Some interesting ones to try:

  • System.Text.Json — the modern .NET JSON parser. Faster and more memory-efficient than Newtonsoft. Try loading it via reflection and parsing a payload without the Newtonsoft.Json dependency.
  • Microsoft.Identity.Client — MSAL is in the AppDomain. Whether you can actually acquire tokens from inside a connector code-behind without credentials in scope is an open question — but the types are there.
  • Azure.Core and Azure.Data.Tables — the full Azure SDK is present. Whether these can make outbound calls from inside the connector pipeline, or whether Context.SendAsync is the only permitted outbound path, is worth testing.
  • System.Reflection.Emit — IL generation at runtime. You could theoretically generate and execute code dynamically, not just load pre-compiled assemblies.

Direction 2 — Documented namespaces nobody uses

The official list has entries that almost no connector tutorial ever mentions. Some are genuinely useful:

  • System.Drawing, System.Drawing.Drawing2D, System.Drawing.Imaging — GDI+, fully documented and supported. You can generate images, draw text, resize photos, or read EXIF metadata directly inside a connector operation. No external library needed. This is the most underused capability in the entire list.
  • System.IO.Compression — GZip and Deflate compression/decompression. Useful for connectors that need to handle compressed payloads from APIs that send gzipped responses outside of what HttpClient handles automatically.
  • System.Net.Security — SSL/TLS stream handling. Potentially useful for custom certificate validation scenarios.
  • System.WebHttpUtility for URL and HTML encoding/decoding. A small utility but saves writing your own encoding logic.

🎯 The experiment template

The connector setup from this post — a single ReflectionTest operation that returns a JSON object — is a reusable lab template. Add a class to MyUtilLib, rebuild, regenerate the Base64, update the connector. Each class tests one capability. The assembliesLoaded field tells you what’s available before you even write the first test.


What This Experiment Confirms

The runtime is Azure Functions on .NET 6, hosted by the Azure WebJobs Script host, with the full Azure SDK, MSAL, ASP.NET Core, and Roslyn all present in the process.

The .NET Standard 2.0 label is a compilation target. The namespace list is a Roslyn enforcement boundary. Neither describes the actual runtime.

Assembly.Load(byte[]) works. System.Globalization is reachable. JWT decoding — something Power Automate expressions can’t do natively without five separate steps — runs as a single connector action.

The full runtime stack: Azure Functions on .NET 6, Roslyn compiling your code at runtime, and the Power Platform connector layer wrapping the execution.

The namespace list is the gate. The runtime is what’s on the other side.