PowerShell is everywhere in Windows automation. Writing it by hand is fine, but generating it programmatically from C# backends has always been string soup. SeaShell fixes that: you write C#, it generates PowerShell at compile time.
Before SeaShell:
// Generating PS the old way
var script = new StringBuilder();
script.AppendLine($"$env = \"{environment}\"");
script.AppendLine("if (Test-Path $deployPath) {");
script.AppendLine(" Stop-Service -Name W3SVC");
// ... 40 more lines of thisAfter SeaShell:
[PowerShellScript]
public static void Deploy(string environment, string deployPath)
{
if (!Directory.Exists(deployPath))
return;
ServiceController.Stop("W3SVC");
Console.WriteLine($"Deployed to {environment}");
}
// That's it. SeaShell generates the PS at compile time.dotnet add package SeaShell.Attributes
dotnet add package SeaShell.RuntimeAdd the generator as an analyzer:
<PackageReference Include="SeaShell.Generator" Version="0.2.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />using SeaShell;
public partial class HelloWorldSamples
{
[PowerShellScript]
public static void SayHello(string name)
{
string greeting = $"Hello, {name}!";
Console.WriteLine(greeting);
}
}SeaShell generates:
param(
[Parameter(Mandatory=$true)]
[string]$name
)
$greeting = "Hello, $name!"
Write-Host $greetingAnd it gives you a C# accessor:
var ps = HelloWorldSamples.SayHelloScript.PowerShell;
await HelloWorldSamples.SayHelloScript.ExecuteAsync("Sarvesh");
HelloWorldSamples.SayHelloScript.SaveTo("say-hello.ps1");[PowerShellScript]
public static void DeployApplication(
string environment,
string buildPath,
string deployPath,
string[] services)
{
string backupDir = $@"C:\Backups\{environment}";
if (!Directory.Exists(deployPath))
{
Console.Error.WriteLine($"Deploy path not found: {deployPath}");
return;
}
Directory.CreateDirectory(backupDir);
Directory.Copy(deployPath, backupDir);
try
{
foreach (var svc in services)
ServiceController.Stop(svc);
Directory.Delete(deployPath);
Directory.Copy(buildPath, deployPath);
foreach (var svc in services)
ServiceController.Start(svc);
}
catch (Exception err)
{
Console.Error.WriteLine($"Deployment failed: {err.Message}");
}
}That kind of script is where SeaShell starts to feel useful. You keep the shape of ordinary C# control flow, and the generated PowerShell is still inspectable.
var result = await MonitoringSamples.CheckDiskSpaceScript.ExecuteAsync(
driveName: "C",
thresholdGb: 10);
if (!result.Success)
await AlertService.SendAsync(result.ErrorOutput);This is the pattern SeaShell is built around: generate the script at compile time, execute it when you need to, then let your C# app react to output, errors, and exit codes.
| C# | PowerShell |
|---|---|
string name = "A" |
$name = "A" |
bool ok = true |
$ok = $true |
int count = 42 |
$count = 42 |
Console.WriteLine(x) |
Write-Host $x |
Console.Error.WriteLine(x) |
Write-Error $x |
if / else if / else |
if / elseif / else |
foreach |
foreach ($item in $items) |
for, while, do while |
PowerShell loops |
try / catch / finally |
PowerShell error-handling blocks |
return, break, continue |
Same keywords |
switch |
PowerShell switch |
==, !=, >, <, &&, ` |
|
| String interpolation | PowerShell interpolation |
| Method parameters | param() block |
| C# | PowerShell |
|---|---|
Directory.Exists(path) |
Test-Path -PathType Container |
Directory.CreateDirectory(path) |
New-Item -ItemType Directory -Force |
Directory.Delete(path) |
Remove-Item -Force |
Directory.Copy(src, dst) |
Copy-Item -Recurse |
File.Exists(path) |
Test-Path -PathType Leaf |
File.ReadAllText(path) |
Get-Content -Raw |
File.WriteAllText(path, value) |
Set-Content |
File.AppendAllText(path, value) |
Add-Content |
Path.Combine(a, b) |
Join-Path |
ServiceController.Stop(name) |
Stop-Service -Force |
ServiceController.Start(name) |
Start-Service |
Process.Start(file) |
Start-Process -Wait |
Environment.GetEnvironmentVariable(name) |
$env:name |
Environment.SetEnvironmentVariable(name, value) |
$env:name = value |
DateTime.Now |
Get-Date |
String.IsNullOrEmpty(value) |
[string]::IsNullOrEmpty(value) |
Convert.ToInt32(value) |
[int]value |
Thread.Sleep(ms) |
Start-Sleep -Milliseconds ms |
Some PowerShell is too specific to pretend it is normal C#. That is what [RawPS] is for.
[RawPS("Get-LocalUser -Name $username -ErrorAction SilentlyContinue")]
object existingUser = default!;SeaShell emits the expression exactly:
$existingUser = Get-LocalUser -Name $username -ErrorAction SilentlyContinueUse it for Active Directory, WMI, registry, certificates, vendor modules, and one-off commands that are clearer as PowerShell.
Current preview note: C# does not compile attributes directly on local variable declarations, so [RawPS] local-variable examples document the intended generator syntax while the compiler limitation is tracked by tests. Until that syntax is moved to a compiler-valid shape, use the fluent builder or plain PowerShell for those escape-hatch cases.
| Code | Meaning | Fix |
|---|---|---|
SS001 |
[PowerShellScript] method is not static |
Make the method static |
SS002 |
Containing class is not partial | Mark the class partial |
SS003 |
C# syntax is not supported | Rewrite as supported C# or use [RawPS] |
SS004 |
BCL method has no mapping | Use [RawPS] or add a mapping |
SS010 |
LINQ is not supported yet | Rewrite as foreach |
SS011 |
async/await is not translated |
Keep generated scripts synchronous |
Generated scripts run through PowerShell 7 (pwsh) on Linux and macOS. File, path, process, environment, JSON, and many shell operations work well cross-platform. Windows-only PowerShell stays Windows-only: Active Directory, WMI, registry providers, firewall rules, Windows services, and certificate store automation depend on the host.
Write portable scripts by sticking to pwsh cmdlets, Path helpers, environment variables, and [RawPS] blocks that branch on $IsWindows, $IsLinux, or $IsMacOS.
| Version | Focus |
|---|---|
v0.3 |
LINQ expansion and more collection mappings |
v1.0 |
Full mapping coverage for the core automation surface |
