UnifyWeaver

Chapter 4: .NET Integration

This chapter covers accessing .NET types from PowerShell scripts generated by UnifyWeaver. You’ll learn about inline C# code, the dotnet_source plugin, NuGet package integration, and DLL caching for performance.

Why .NET?

PowerShell is built on .NET, giving you access to:

UnifyWeaver’s dotnet_source plugin lets you embed C# code directly in Prolog predicates, generating PowerShell scripts that compile and execute .NET code at runtime.

Inline C# with Add-Type

Basic Add-Type

PowerShell’s Add-Type compiles C# code at runtime:

Add-Type @'
using System;

public class StringHelper {
    public static string Reverse(string input) {
        char[] chars = input.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }
}
'@

# Use the compiled class
[StringHelper]::Reverse("Hello World")
# Output: dlroW olleH

UnifyWeaver’s dotnet_source Plugin

The dotnet_source plugin generates this pattern from Prolog:

:- use_module(library(dotnet_source)).

% Define C# code
CSharpCode = '
using System;

namespace UnifyWeaver.Generated.StringReverser {
    public class StringReverserHandler {
        public string ProcessStringReverser(string input) {
            if (string.IsNullOrEmpty(input)) {
                return input;
            }
            char[] charArray = input.ToCharArray();
            Array.Reverse(charArray);
            return new string(charArray);
        }
    }
}
',

% Compile to PowerShell
Config = [csharp_inline(CSharpCode)],
dotnet_source:compile_source(string_reverser/2, Config, [], PowerShellCode).

Generated PowerShell

The plugin generates a complete PowerShell script:

# string_reverser - .NET inline compilation source
# Generated by UnifyWeaver

function string_reverser {
    param([Parameter(ValueFromPipeline=$true)]$InputData)

    begin {
        # Compile C# code (first run only)
        $csharpCode = @'
using System;

namespace UnifyWeaver.Generated.StringReverser {
    public class StringReverserHandler {
        public string ProcessStringReverser(string input) {
            // ... implementation
        }
    }
}
'@
        Add-Type -TypeDefinition $csharpCode -Language CSharp

        # Create instance
        $handler = New-Object UnifyWeaver.Generated.StringReverser.StringReverserHandler
    }

    process {
        if ($InputData) {
            $handler.ProcessStringReverser($InputData)
        }
    }
}

# Auto-execute when run directly
if ($MyInvocation.InvocationName -ne '.') {
    string_reverser @args
}

External Compilation with dotnet build

For more complex scenarios requiring NuGet packages, the plugin can generate external compilation:

Prolog Configuration

Config = [
    csharp_inline(CSharpCode),
    compile_mode(external),           % Use dotnet build
    references(['System.Text.Json'])  % NuGet packages
],
dotnet_source:compile_source(json_validator/2, Config, [], PowerShellCode).

Generated Structure

The generated script:

  1. Creates a temporary .NET project
  2. Writes .csproj with NuGet references
  3. Writes C# source file
  4. Runs dotnet restore and dotnet build
  5. Loads the compiled DLL
  6. Creates an instance and calls the method
  7. Cleans up temporary files
function json_validator {
    param([Parameter(ValueFromPipeline=$true)]$InputData)

    begin {
        $projectName = "json_validator_Handler"
        $tempBase = if ($env:TEMP) { $env:TEMP } else { "/tmp" }
        $projectDir = Join-Path $tempBase "unifyweaver_dotnet_build/$projectName"

        # Write .csproj
        $csprojContent = @'
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.Text.Json" Version="8.0.0" />
  </ItemGroup>
</Project>
'@
        Set-Content -Path "$projectDir/$projectName.csproj" -Value $csprojContent

        # Write C# code
        $csFile = "$projectDir/Handler.cs"
        Set-Content -Path $csFile -Value $csharpCode

        # Build
        dotnet restore $projectDir
        dotnet build $projectDir --no-restore

        # Load DLL
        $dll = "$projectDir/bin/Debug/net8.0/$projectName.dll"
        Add-Type -Path $dll

        # Create instance
        $handler = New-Object UnifyWeaver.Generated.JsonValidator.Handler
    }

    process {
        $handler.Process($InputData)
    }

    end {
        # Cleanup
        Remove-Item -Recurse -Force $projectDir
    }
}

DLL Caching for Performance

Without caching, every invocation recompiles C#. The pre_compile option enables caching:

Performance Comparison

Mode First Run Subsequent Runs
No cache ~386ms ~386ms
With cache ~386ms ~2.8ms

That’s a 138x speedup for cached runs!

Enabling Caching

Config = [
    csharp_inline(CSharpCode),
    pre_compile(true)  % Enable DLL caching
],
dotnet_source:compile_source(string_reverser/2, Config, [], PowerShellCode).

How Caching Works

  1. Hash the source: Generate SHA256 of C# code
  2. Check cache: Look for $env:TEMP/unifyweaver_dotnet_cache/{hash}.dll
  3. Cache hit: Load existing DLL directly
  4. Cache miss: Compile, save to cache, then load
begin {
    $codeHash = Get-StringHash $csharpCode
    $cachePath = "$env:TEMP/unifyweaver_dotnet_cache"
    $cachedDll = "$cachePath/$codeHash.dll"

    if (Test-Path $cachedDll) {
        Write-Verbose "Loading from cache: $cachedDll"
        Add-Type -Path $cachedDll
    } else {
        Write-Verbose "Compiling and caching..."
        # ... compile ...
        Copy-Item $compiledDll $cachedDll
        Add-Type -Path $cachedDll
    }
}

Common .NET Patterns

JSON Processing

CSharpCode = '
using System;
using System.Text.Json;

namespace UnifyWeaver.Generated.JsonParser {
    public class Handler {
        public string Process(string jsonString, string path) {
            using JsonDocument doc = JsonDocument.Parse(jsonString);
            JsonElement element = doc.RootElement;

            foreach (string part in path.Split(".")) {
                element = element.GetProperty(part);
            }

            return element.GetRawText();
        }
    }
}
',
Config = [csharp_inline(CSharpCode)],
dotnet_source:compile_source(json_get/3, Config, [], Code).

Usage:

$json = '{"user": {"name": "Alice", "age": 30}}'
json_get $json "user.name"
# Output: "Alice"

CSV Processing

CSharpCode = '
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace UnifyWeaver.Generated.CsvReader {
    public class Handler {
        public IEnumerable<string[]> Process(string filePath) {
            return File.ReadAllLines(filePath)
                .Select(line => line.Split(","));
        }
    }
}
',
Config = [csharp_inline(CSharpCode)],
dotnet_source:compile_source(csv_rows/2, Config, [], Code).

LiteDB Integration

CSharpCode = '
using System;
using System.Collections.Generic;
using LiteDB;

namespace UnifyWeaver.Generated.LiteDbQuery {
    public class Handler {
        public IEnumerable<BsonDocument> Process(string dbPath, string collection) {
            using var db = new LiteDatabase(dbPath);
            var col = db.GetCollection<BsonDocument>(collection);
            return col.FindAll().ToList();
        }
    }
}
',
Config = [
    csharp_inline(CSharpCode),
    compile_mode(external),
    references(['LiteDB'])
],
dotnet_source:compile_source(litedb_query/3, Config, [], Code).

XML Transformation

CSharpCode = '
using System;
using System.Xml.Linq;
using System.Linq;
using System.Collections.Generic;

namespace UnifyWeaver.Generated.XmlExtractor {
    public class Handler {
        public IEnumerable<string> Process(string xmlPath, string elementName) {
            XDocument doc = XDocument.Load(xmlPath);
            return doc.Descendants(elementName)
                .Select(e => e.Value);
        }
    }
}
',
Config = [csharp_inline(CSharpCode)],
dotnet_source:compile_source(xml_extract/3, Config, [], Code).

Type Conversions

PowerShell to .NET

PowerShell .NET Type
[string] System.String
[int] System.Int32
[double] System.Double
[bool] System.Boolean
[datetime] System.DateTime
@(1,2,3) System.Object[]
@{a=1} System.Collections.Hashtable

.NET to PowerShell

.NET objects are automatically wrapped. Access properties directly:

# .NET List<string>
$list = [System.Collections.Generic.List[string]]::new()
$list.Add("item")
$list.Count  # Property access

# Custom class from Add-Type
$handler = New-Object MyNamespace.MyClass
$handler.MyMethod("arg")  # Method call
$handler.MyProperty       # Property access

Collections

# Array to List
$array = @(1, 2, 3)
$list = [System.Collections.Generic.List[int]]$array

# List to Array
$array2 = $list.ToArray()

# Dictionary
$dict = [System.Collections.Generic.Dictionary[string,int]]::new()
$dict.Add("key", 42)
$dict["key"]  # Access by key

Namespace Conventions

UnifyWeaver uses consistent namespaces:

UnifyWeaver.Generated.<PredicateName>
    └─ <PredicateName>Handler
        └─ Process<PredicateName>(args...)

Example for json_validator/2:

namespace UnifyWeaver.Generated.JsonValidator {
    public class JsonValidatorHandler {
        public string ProcessJsonValidator(string input) {
            // ...
        }
    }
}

This convention:

Error Handling in .NET Code

Try/Catch in C#

public string Process(string input) {
    try {
        // Risky operation
        return DoSomething(input);
    }
    catch (ArgumentException ex) {
        return $"ERROR: Invalid argument - {ex.Message}";
    }
    catch (Exception ex) {
        return $"ERROR: {ex.GetType().Name} - {ex.Message}";
    }
}

Surfacing Errors to PowerShell

process {
    $result = $handler.Process($InputData)

    if ($result -match '^ERROR:') {
        Write-Error $result
    } else {
        $result
    }
}

Complete Example: JSON to LiteDB Pipeline

Let’s build a complete pipeline that reads JSON, validates it, and stores it in LiteDB.

Step 1: JSON Validator

JsonValidatorCode = '
using System;
using System.Text.Json;

namespace UnifyWeaver.Generated.JsonValidator {
    public class Handler {
        public bool Process(string jsonString) {
            try {
                JsonDocument.Parse(jsonString);
                return true;
            }
            catch {
                return false;
            }
        }
    }
}
'.

Step 2: LiteDB Writer

LiteDbWriterCode = '
using System;
using LiteDB;

namespace UnifyWeaver.Generated.LiteDbWriter {
    public class Handler {
        public int Process(string dbPath, string collection, string json) {
            using var db = new LiteDatabase(dbPath);
            var col = db.GetCollection<BsonDocument>(collection);
            var doc = BsonMapper.Global.Deserialize<BsonDocument>(json);
            col.Insert(doc);
            return col.Count();
        }
    }
}
'.

Step 3: Compile Both

:- use_module(library(dotnet_source)).

compile_pipeline :-
    % JSON Validator (inline compile)
    dotnet_source:compile_source(json_validate/2,
        [csharp_inline(JsonValidatorCode), pre_compile(true)],
        [], ValidatorCode),

    % LiteDB Writer (external compile with NuGet)
    dotnet_source:compile_source(litedb_write/4,
        [csharp_inline(LiteDbWriterCode), compile_mode(external),
         references(['LiteDB']), pre_compile(true)],
        [], WriterCode),

    % Write scripts
    open('output/json_validate.ps1', write, S1),
    write(S1, ValidatorCode), close(S1),

    open('output/litedb_write.ps1', write, S2),
    write(S2, WriterCode), close(S2).

Step 4: Use in PowerShell

# Load functions
. ./output/json_validate.ps1
. ./output/litedb_write.ps1

# Process JSON files
Get-ChildItem *.json | ForEach-Object {
    $content = Get-Content $_ -Raw

    if (json_validate $content) {
        Write-Host "Valid: $_" -ForegroundColor Green
        litedb_write "data.db" "records" $content
    } else {
        Write-Warning "Invalid JSON: $_"
    }
}

What’s Next?

In Chapter 5, you’ll learn about Windows automation:

Quick Reference

dotnet_source Options

Option Values Description
csharp_inline(Code) String C# source code
compile_mode(Mode) inline, external Compilation method
pre_compile(Bool) true/false Enable DLL caching
references(List) Package names NuGet dependencies

Common .NET Namespaces

Namespace Purpose
System Core types
System.IO File operations
System.Text.Json JSON parsing
System.Xml.Linq XML processing
System.Collections.Generic Collections
System.Linq LINQ queries
System.Net.Http HTTP client
System.Text.RegularExpressions Regex

PowerShell .NET Shortcuts

# Create instance
$obj = New-Object Namespace.Class
$obj = [Namespace.Class]::new()

# Static method
[System.IO.Path]::GetExtension("file.txt")

# Generic type
[System.Collections.Generic.List[string]]::new()

# Load assembly
Add-Type -AssemblyName System.Web

File Locations

Path Description
src/unifyweaver/sources/dotnet_source.pl Plugin implementation
playbooks/powershell_inline_dotnet_playbook.md Detailed guide
examples/powershell_dotnet_example.pl Working examples

← Previous: Chapter 3: Cmdlet Generation 📖 Book 12: PowerShell Target Next: Chapter 5: Windows Automation →