I’ve been writing Python scripts for years to automate tasks I could do better in C#. Why? Because Python felt light. Write a file, run it. C# required project files, bin folders, and ceremony.

That changes with .NET 10, released November 12, 2025.

The Barrier That’s Gone

Before .NET 10, even the simplest C# script needed infrastructure:

dotnet new console -n MyScript
cd MyScript
# Now you have .csproj, bin/, obj/, Program.cs

That overhead killed C# for quick scripts. Python won by default because it had less friction.

Why It Took So Long

.NET Framework had full ceremony. .NET 6 added top-level statements but still required .csproj files. .NET 10 finally removes the last barrier: you can now run a single .cs file directly.

Shebang Support: C# Scripts That Run Like Bash

.NET 10 introduces shebang support on Linux/macOS. Create a .cs file with #!/usr/bin/env dotnet and it’s executable:

#!/usr/bin/env dotnet

Console.WriteLine($"Hello {args.FirstOrDefault() ?? "World"}");

Make it executable and run:

chmod +x hello.cs
./hello.cs Claude
# Output: Hello Claude

No dotnet run, no project file. It’s a script.

Arg Parsing Edge Case

Command names like “list” or “build” now work correctly as script arguments in .NET 10 RTM. However, dotnet flags like --help or --version still get intercepted by the CLI. If your script needs these as arguments, use #!/usr/bin/env dotnet -- to separate dotnet options from script args.

The Secret Sauce: Ignored Directives

Ignored directives are .NET 10’s killer feature. They’re special comments that configure your script without a project file.

Available directives:

  • #:package PackageName@Version - Add NuGet packages
  • #:sdk 10.0.100 - Pin SDK version
  • #:property Name Value - Set MSBuild properties
  • #:project path/to.csproj - Reference other projects

The package directive changes everything. You can pull in any NuGet package inline:

#!/usr/bin/env dotnet
#:package Microsoft.Data.SqlClient@5.2.2

using Microsoft.Data.SqlClient;

// Full MSSQL client in a single-file script

This is Python’s import but with NuGet’s entire ecosystem. Type-safe. Async/await. Compiled.

Real Example: MSSQL CLI Tool

I replaced my Python database scripts with this C# tool. It queries MSSQL, lists databases, and shows tables:

#!/usr/bin/env dotnet
#:package Microsoft.Data.SqlClient@5.2.2

using Microsoft.Data.SqlClient;

if (args.Length == 0 || args[0] == "--help")
{
    Console.WriteLine("MSSQL CLI Query Tool");
    Console.WriteLine("Usage:");
    Console.WriteLine("  ./sqlquery.cs list");
    Console.WriteLine("  ./sqlquery.cs [db] [query]");
    Console.WriteLine("  ./sqlquery.cs [db] tables");
    Environment.Exit(0);
}

var server = "localhost,1433";
var userId = "sa";
var password = Environment.GetEnvironmentVariable("SQL_PASSWORD") ?? "DefaultPass";

if (args[0] == "list")
{
    await ListDatabases(server, userId, password);
}
else if (args.Length >= 2)
{
    var database = args[0];
    var query = args[1] == "tables"
        ? "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'"
        : args[1];

    await ExecuteQuery(server, database, userId, password, query);
}

async Task ListDatabases(string server, string userId, string password)
{
    var connStr = $"Data Source={server};User ID={userId};Password={password};TrustServerCertificate=True;";
    using var conn = new SqlConnection(connStr);
    await conn.OpenAsync();

    var cmd = new SqlCommand("SELECT name FROM sys.databases WHERE database_id > 4", conn);
    using var reader = await cmd.ExecuteReaderAsync();

    Console.WriteLine("Available databases:");
    while (await reader.ReadAsync())
    {
        Console.WriteLine($"  - {reader.GetString(0)}");
    }
}

async Task ExecuteQuery(string server, string database, string userId, string password, string query)
{
    var connStr = $"Data Source={server};Initial Catalog={database};User ID={userId};Password={password};TrustServerCertificate=True;";

    using var conn = new SqlConnection(connStr);
    await conn.OpenAsync();

    var cmd = new SqlCommand(query, conn);
    using var reader = await cmd.ExecuteReaderAsync();

    // Print headers
    var columns = new List<string>();
    for (int i = 0; i < reader.FieldCount; i++)
        columns.Add(reader.GetName(i));

    Console.WriteLine(string.Join(" | ", columns));
    Console.WriteLine(new string('-', columns.Sum(c => c.Length) + (columns.Count - 1) * 3));

    // Print rows
    int rowCount = 0;
    while (await reader.ReadAsync())
    {
        var values = new List<string>();
        for (int i = 0; i < reader.FieldCount; i++)
            values.Add(reader.IsDBNull(i) ? "NULL" : reader.GetValue(i).ToString());

        Console.WriteLine(string.Join(" | ", values));
        rowCount++;
    }

    Console.WriteLine($"\n{rowCount} row(s) returned");
}

Usage:

chmod +x sqlquery.cs

./sqlquery.cs list
# Available databases:
#   - myapp
#   - myapp_logs

./sqlquery.cs myapp "SELECT TOP 3 username FROM Users"
# username
# --------------------
# john.doe
# jane.smith
# admin
#
# 3 row(s) returned

This replaces a Python script with pyodbc and all its dependency hell. One file, no virtual env, type-safe queries.

The Native Library Advantage

This is the “write libraries, not MCPs” approach from Expressing MCP Tools as Code APIs. Database MCPs add protocol overhead for zero benefit. A direct SQL client script costs zero context tokens, has full type safety, and no translation layer.

For Claude Code users: C# scripts work perfectly as native libraries - Claude executes them via the Bash tool. Database access? This script. HTTP clients? HttpClient. JSON parsing? System.Text.Json. Same benefits as TypeScript, different ecosystem.

The moment I could pull NuGet packages inline, I stopped context-switching. Why learn another language’s ecosystem when mine just became scriptable?

— Developer who switched from Python scripts

PowerShell to C#: File Operations

PowerShell devs lean on Get-ChildItem and pipeline commands for file ops. Here’s a common task (finding large files) migrated to C#:

PowerShell:

Get-ChildItem -Path $path -Recurse -File |
  Where-Object {$_.Length -gt $minSize} |
  Sort-Object Length -Descending |
  Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}}

C# (.NET 10):

#!/usr/bin/env dotnet

var path = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
var minSizeMB = args.Length > 1 ? int.Parse(args[1]) : 10;
var minSizeBytes = minSizeMB * 1024 * 1024;

Console.WriteLine($"Files larger than {minSizeMB}MB in: {path}\n");

var largeFiles = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
    .Select(f => new FileInfo(f))
    .Where(f => f.Length > minSizeBytes)
    .OrderByDescending(f => f.Length)
    .ToList();

if (largeFiles.Count == 0)
{
    Console.WriteLine("No large files found.");
    return;
}

Console.WriteLine($"{"Size (MB)",-12} {"File Path"}");
Console.WriteLine(new string('-', 80));

foreach (var file in largeFiles)
{
    var sizeMB = Math.Round(file.Length / (1024.0 * 1024.0), 2);
    Console.WriteLine($"{sizeMB,-12:N2} {file.FullName}");
}

var totalGB = Math.Round(largeFiles.Sum(f => f.Length) / (1024.0 * 1024.0 * 1024.0), 2);
Console.WriteLine($"\nFound {largeFiles.Count} file(s) totaling {totalGB:N2} GB");

Usage:

chmod +x find-large-files.cs
./find-large-files.cs /var/log 50

C# is more verbose than PowerShell, but it’s beautiful to look at and understand. The LINQ chain reads clearly: enumerate files, filter by size, order by length, format output. No pipeline operators requiring mental parsing, no cryptic $_ references, no hash table syntax for computed properties. Type safety means your editor catches errors before you run the script. The verbosity is the point - explicit beats clever.

When to Use C# vs Python/Bash

Use C#:

  • Type safety matters (production automation, CI/CD)
  • Team already knows .NET
  • Cross-platform deployment needed
  • Script will grow (they always do)
  • Need async/await for I/O-heavy tasks
  • Want to compile to native binary (NativeAOT) later

Use Python:

  • Data science / ML workflows
  • One-liners and quick text processing
  • Mature library for specific domain (requests, pandas, numpy)
  • Team knows Python better

Use Bash:

  • Shell operations (piping between unix tools)
  • Quick glue scripts under 10 lines
  • CI/CD steps that wrap other commands
Hybrid Approach

Use C# for the heavy lifting (HTTP clients, DB queries, parsing), then call it from bash scripts when needed. Best of both worlds.

What .NET 10 Doesn’t Solve

Let’s be honest about the gaps:

  • Verbosity: C# is still wordier than Python for simple tasks
  • Startup time: JIT compilation adds latency vs interpreted languages (compile to NativeAOT if this matters)
  • Ecosystem for certain domains: Python dominates ML, data science, web scraping
  • Syntax familiarity: Most ops teams know bash/Python better than C#
  • Package discovery: NuGet isn’t as script-friendly as pip search yet

These aren’t dealbreakers, but they’re real. If your task is “parse this CSV and print column 3”, bash or Python is still faster to write.

Deployment: Single-File Binaries

The hidden benefit: C# scripts compile. Once your script is stable, compile it to a single-file executable:

dotnet publish -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true sqlquery.cs

Now you have a native binary with no .NET runtime dependency. Deploy it anywhere. Python can’t do this without PyInstaller hacks.

The LTS Benefit

.NET 10 is an LTS release (supported until November 2028). Your scripts won’t break in 6 months.

Compare to Python 2 → 3 migration pain, or Node’s breaking changes every other version. .NET’s stability matters for long-lived automation.

We switched our ops scripts to C# after .NET 10. Three years of support, native binaries, and the same language our backend uses. No more maintaining two ecosystems.

— SRE team lead

Try It

Install .NET 10:

# macOS
brew install dotnet@10

# Linux (Ubuntu)
wget https://dot.net/v1/dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 10.0

Create hello.cs:

#!/usr/bin/env dotnet
Console.WriteLine("C# as a scripting language is real");

Run it:

chmod +x hello.cs
./hello.cs

If you’re a .NET dev still writing Python scripts for automation, .NET 10 removes your excuse. Try it for your next build script or DevOps tool.

The scripting gap is closed.