Met behulp van CSX Scripts voor Quick C# Testen (Nederlands (Dutch))

Met behulp van CSX Scripts voor Quick C# Testen

Wednesday, 26 November 2025

//

28 minute read

Wilt u een knipsel van C#-code testen zonder een volledig project te draaien? C# Script-bestanden (.csx) laat je C#-code schrijven en uitvoeren als een scripttaal. Nee Program.cs, nee .csproj, geen bouwstap - gewoon schrijven en uitvoeren. Perfect voor het testen van API's, het valideren van logica, of prototyping alvorens zich te verbinden tot een volledige implementatie.

Voor ik rol de volledige semanti c zoekfunctie Ik dacht dat ik zou delen hoe ik gebruik .csx dossiers voor ad-hoctests in deze en andere projecten.

CSX vs .NET 10 File-based Apps

Alvorens in te duiken, laten we ons richten op de olifant in de kamer: .NET 10 heeft nu inheemse "file-based apps" waarmee u kunt draaien .cs bestanden direct met dotnet run app.csHoe verhoudt dit zich tot CSX?

.NET 10 File-based Apps (Nu beschikbaar!)

Met .NET 10, kunt u een enkel bestand C# direct uitvoeren:

# .NET 10 - available now!
dotnet run app.cs

Functies:

  • Inheems aan de SDK - geen extra tools nodig
  • Gebruikt vertrouwd .cs uitbreiding
  • NuGet referenties via #:package richtlijn
  • Volledige debugger ondersteuning vanaf dag één
  • Zelfde compiler als gewone projecten
// app.cs - .NET 10 style
#:package [email protected]

using Newtonsoft.Json;

var obj = new { Name = "Test", Value = 42 };
Console.WriteLine(JsonConvert.SerializeObject(obj));

CSX Scripts (Beschikbaar nu)

CSX via dotnet-script bestaat sinds 2017:

# Available today
dotnet script app.csx

Functies:

  • Werkt met .NET 6, 7, 8, 9 , 10
  • Rijk ecosysteem en tooling
  • REPL-modus voor interactieve exploratie
  • Bewezen en gevochten
// app.csx - CSX style
#r "nuget: Newtonsoft.Json, 13.0.3"

using Newtonsoft.Json;

var obj = new { Name = "Test", Value = 42 };
Console.WriteLine(JsonConvert.SerializeObject(obj));

Welke moet je gebruiken?

Kenmerk CSX (dotnet-script) * .NET 10 File Apps * |---------|---------------------|-------------------| | Beschikbaarheid .NET 6+ .NET 10 | Installatie | dotnet tool install -g dotnet-script Ingebouwd in SDK | Bestandsextensie | .csx | .cs | | NuGet Syntaxis | #r "nuget: Pkg, Ver" | #:package Pkg@Ver | | REPL-modus - Ja, nog niet. | IDE-ondersteuning Goed (VS-code, ruiter) Verbeteren | Debuggen Ja (inheems)

Mijn aanbeveling:

  • Probeer eerst .NET 10 bestandsapps - inheemse ondersteuning betekent minder gereedschap overhead
  • Terugtrekken naar CSX als u REPL-modus nodig heeft of op een oudere .NET-versie staat
  • De concepten zijn bijna identiek - kennisoverdracht gemakkelijk tussen beide

De rest van dit artikel heeft betrekking op CSX die nog steeds geweldig werkt en heeft een aantal functies (zoals REPL) die .NET 10 bestandsapps nog niet hebben.

Wat is CSX?

CSX (C# Script) bestanden zijn C# code bestanden die direct kunnen worden uitgevoerd zonder compilatie in een project. Zie het als "Python-style" C# - je schrijft code, je voert het uit, je ziet resultaten.

// hello.csx
Console.WriteLine("Hello from C# Script!");

Voer het uit:

dotnet script hello.csx

Dat is het. Main() methode, geen naamruimte, geen klasse-wrapper vereist.

Dotnet-script installeren

De meest populaire manier om CSX-bestanden uit te voeren is via dotnet-script:

dotnet tool install -g dotnet-script

Verifiëren installatie:

dotnet script --version

Waar CSX past in de testcyclus

Voordat we in de "hoe" duiken, laten we de "wanneer" begrijpen. CSX scripts bezetten een unieke plek in de testpiramide:

                    ┌─────────────────┐
                    │   E2E Tests     │  ← Full system, slow, expensive
                    │   (Playwright)  │
                   ─┼─────────────────┼─
                   │ Integration Tests │  ← Multiple components, database
                   │    (xUnit + DB)   │
                  ─┼───────────────────┼─
                 │   CSX Scripts        │  ← Quick validation, exploration
                 │   (Ad-hoc testing)   │     ★ YOU ARE HERE ★
                ─┼─────────────────────┼─
               │      Unit Tests         │  ← Single class, mocked deps
               │   (xUnit, NUnit, etc)   │
              ─┴─────────────────────────┴─

CSX Scripts als "Mini integratietests"

CSX scripts zijn geen vervanging voor formele tests - ze zijn een aanvullingZie ze als:

  • Pre-implementatie pieken - Controleer een API werkt voordat het bouwen van een dienst eromheen
  • Hulpmiddelen voor debuggen - Isoleer en reproduceer problemen zonder uw hele app opnieuw op te bouwen
  • Verkennende tests - Begrijpen hoe een bibliotheek zich gedraagt voor het schrijven van unit tests
  • Rooktests - Snelle sanity controles met betrekking tot echte diensten (database, API's, wachtrijen)

Ontwikkelingswerk

Hier is hoe CSX past in een typische functie ontwikkeling cyclus:

1. EXPLORE (CSX Script)
   └─→ "Does this API even work? What's the response format?"
   └─→ Write a quick script to call the API and see the output

2. PROTOTYPE (CSX Script)
   └─→ "How should I structure this service?"
   └─→ Test different approaches without project scaffolding

3. IMPLEMENT (Production Code)
   └─→ Build the actual service with proper error handling, DI, etc.
   └─→ You already know the API works from step 1!

4. TEST (xUnit/NUnit)
   └─→ Write formal unit tests with mocks
   └─→ Write integration tests against test database

5. DEBUG (CSX Script)
   └─→ Production issue? Write a script to reproduce it
   └─→ Faster than adding logging, rebuilding, deploying

Real Voorbeeld: Bouwen van de Umami integratie

Toen ik de Umami analytics integratie voor deze blog bouwde, was mijn workflow:

  1. CSX: Test de ruwe API Hoe zien reacties eruit?
  2. CSX: Testtijdstempelconversie - Ik vond hier een bug voordat ik een productiecode schreef!
  3. Implementatie: UmamiClient bouwen - Met vertrouwen omdat ik de API al had gevalideerd
  4. xEenheid: Write unit tests - Mock HttpClient, testserialisatie logica
  5. CSX: Debug productie probleem Script om het probleem te isoleren.

De CSX scripts hebben mijn test niet vervangen. verhinderde me om code te schrijven die niet zou werken en hielp me debug problemen sneller toen ze plaatsvonden.

Waarom CSX gebruiken om te testen?

1. Nul-ceremonie

Traditionele aanpak om een API-oproep te testen:

  1. Nieuw console-project aanmaken
  2. NuGet-pakketten toevoegen
  3. Schrijven Program.cs
  4. Bouwen
  5. Uitvoeren
  6. Project verwijderen indien voltooid

CSX-aanpak:

  1. Schrijf script
  2. Script uitvoeren

2. Inline NuGet referenties

Heb je een pakket nodig? Refereer het direct in je script:

#r "nuget: Newtonsoft.Json, 13.0.3"
#r "nuget: RestSharp, 110.2.0"

using Newtonsoft.Json;
using RestSharp;

var client = new RestClient("https://api.github.com");
var request = new RestRequest("users/scottgal", Method.Get);
request.AddHeader("User-Agent", "CSX-Test");

var response = await client.ExecuteAsync(request);
Console.WriteLine(JsonConvert.SerializeObject(
    JsonConvert.DeserializeObject(response.Content),
    Formatting.Indented));

Eerste run downloads pakketten. Volgende programma's gebruiken cache.

3. Referentie lokale DLL's

Testen van uw eigen bibliotheek? Refereer het direct:

#r "bin/Debug/net9.0/MyLibrary.dll"

using MyLibrary;

var result = MyClass.DoSomething();
Console.WriteLine(result);

4. Referentie andere scripts

Splits complexe scripts in herbruikbare delen:

#load "helpers.csx"
#load "config.csx"

// Use functions/classes from loaded scripts
var config = LoadConfig();
var result = ProcessData(config);

Echte voorbeelden van dit project

Dit zijn geen gekunstelde voorbeelden - het zijn echte scripts die ik gebruik om debug en test deze blog codebase. Elk opgelost een echt probleem dat ik tegenkwam tijdens de ontwikkeling.

API-tijdstempels testen

Het probleem: Mijn Umami analytics integratie was het teruggeven van lege gegevens. Na uren van debuggen, Ik vermoedde dat de tijdstempel conversie verkeerd was - de Umami API verwacht Unix tijdstempels in milliseconden, maar ik was niet zeker of mijn .NET code was het produceren van het juiste formaat.

Waarom CSX? Ik had logging kunnen toevoegen aan de productiecode, herbouwd, ingezet en gecheckt logs. Of ik kon een snel script schrijven om mijn hypothese te verifiëren in 30 seconden.

#!/usr/bin/env dotnet-script

// This script helped debug an issue where the Umami API was returning empty data.
// The API expects Unix timestamps in milliseconds, and I suspected my conversion was wrong.

// Start with known values we can verify
var now = DateTime.UtcNow;
var yesterday = now.AddHours(-24);

// The "O" format specifier gives us ISO 8601 format - precise and unambiguous
// Example output: "2025-11-24T10:30:45.1234567Z"
Console.WriteLine($"Now: {now:O}");
Console.WriteLine($"Yesterday: {yesterday:O}");

// The Umami API expects Unix timestamps in MILLISECONDS (not seconds!)
// DateTimeOffset is the safest way to convert - it handles time zones correctly.
// Always use ToUniversalTime() first to ensure we're working with UTC.
var nowOffset = new DateTimeOffset(now.ToUniversalTime());
var yesterdayOffset = new DateTimeOffset(yesterday.ToUniversalTime());

// ToUnixTimeMilliseconds() returns milliseconds since 1970-01-01 00:00:00 UTC
var nowMs = nowOffset.ToUnixTimeMilliseconds();
var yesterdayMs = yesterdayOffset.ToUnixTimeMilliseconds();

Console.WriteLine($"\nNow in milliseconds: {nowMs}");
Console.WriteLine($"Yesterday in milliseconds: {yesterdayMs}");

// IMPORTANT: Verify the conversion is reversible!
// This catches off-by-one errors and timezone issues
var nowConverted = DateTimeOffset.FromUnixTimeMilliseconds(nowMs);
var yesterdayConverted = DateTimeOffset.FromUnixTimeMilliseconds(yesterdayMs);

Console.WriteLine($"\nConverted back (should match above):");
Console.WriteLine($"Now: {nowConverted:O}");
Console.WriteLine($"Yesterday: {yesterdayConverted:O}");

// THE ACTUAL BUG: I found this timestamp in my application logs
// Let's see what date it actually represents...
var suspiciousTimestamp = 1763440087664L;
var suspiciousDate = DateTimeOffset.FromUnixTimeMilliseconds(suspiciousTimestamp);
Console.WriteLine($"\nSuspicious timestamp {suspiciousTimestamp} = {suspiciousDate:O}");

// Output showed this timestamp was in the year 2025... but it should have been in 2024!
// Tracing back, I found I was using DateTime.Now instead of DateTime.UtcNow,
// causing the local timezone offset to be applied incorrectly.

Het resultaat: Dit script bewees de tijdstempel was 1 jaar in de toekomst. Ik traceerde de bug terug naar het gebruik DateTime.Now in plaats van DateTime.UtcNow in de productiecode. Vast in 5 minuten in plaats van mogelijk 5 uur debuggen.

Testing Query String Generation

Het probleem: Ik moest controleren dat ASP.NET's QueryHelpers klasse genereert query strings in het exacte formaat dat de Umami API verwacht. Heeft het speciale tekens URL-encoderen? Welke volgorde zijn de parameters in?

Waarom CSX? Het lezen van documentatie is één ding, maar het zien van de werkelijke uitvoer vertelt je precies wat je code zal produceren.

#!/usr/bin/env dotnet-script

// Pull in ASP.NET's WebUtilities package - this is the same package
// that ASP.NET Core uses internally for query string manipulation
#r "nuget: Microsoft.AspNetCore.WebUtilities, 9.0.0"

using Microsoft.AspNetCore.WebUtilities;

// These are the exact parameters I need to send to the Umami metrics API
// Using a Dictionary makes it easy to see all parameters at once
var queryParams = new Dictionary<string, string>
{
    {"startAt", "1730000000000"},   // Unix timestamp in milliseconds
    {"endAt", "1730086400000"},     // 24 hours later
    {"type", "url"},                // Type of metric to fetch
    {"unit", "day"},                // Aggregation unit
    {"limit", "500"}                // Maximum results to return
};

// QueryHelpers.AddQueryString builds a properly formatted query string
// First parameter: base URL (empty string = just the query string portion)
// Second parameter: dictionary of key-value pairs
var queryString = QueryHelpers.AddQueryString(string.Empty, queryParams);

Console.WriteLine($"Generated query string:");
Console.WriteLine(queryString);
// Output: ?startAt=1730000000000&endAt=1730086400000&type=url&unit=day&limit=500

// Now let's verify we can parse it back - this catches encoding issues
// that might not be obvious in the generated string
Console.WriteLine($"\nParsed back (verifying round-trip):");
var parsed = QueryHelpers.ParseQuery(queryString);
foreach (var kvp in parsed)
{
    // Note: parsed values are StringValues, not string
    // StringValues can hold multiple values for the same key (e.g., ?tag=a&tag=b)
    Console.WriteLine($"  {kvp.Key} = {kvp.Value}");
}

// What I learned: QueryHelpers properly handles URL encoding for special characters
// This became important when I later added search terms with spaces and unicode

Raw HTTP API-aanroepen testen

Het probleem: Voordat ik een full service class bouw met afhankelijkheidsinjectie, foutafhandeling, retry logica en unit tests, wilde ik controleren of de API daadwerkelijk werkt en het reactieformaat begrijpen.

Waarom CSX? Het is sneller om 50 regels van verkennende code te schrijven dan om een goede service infrastructuur te bouwen. Als de API niet werkt zoals ik verwacht, heb ik 5 minuten verspild in plaats van 5 uur.

#!/usr/bin/env dotnet-script

// System.Net.Http.Json provides extension methods like PostAsJsonAsync and GetFromJsonAsync
// This is the same package ASP.NET Core uses internally
#r "nuget: System.Net.Http.Json, 9.0.0"

using System.Net.Http.Json;
using System.Text.Json;

// Configuration - in a real app these would come from appsettings.json
var websiteId = "32c2aa31-b1ac-44c0-b8f3-ff1f50403bee";
var umamiPath = "https://umami.mostlylucid.net";
var username = "admin";

// SECURITY: Never hardcode passwords! Use environment variables instead.
// Set before running: $env:UMAMI_PASSWORD = "your-password" (PowerShell)
//               or:   export UMAMI_PASSWORD="your-password" (bash)
var password = Environment.GetEnvironmentVariable("UMAMI_PASSWORD") ?? "";

if (string.IsNullOrEmpty(password))
{
    // Provide helpful instructions when the password is missing
    Console.WriteLine("ERROR: Set UMAMI_PASSWORD environment variable");
    Console.WriteLine("  PowerShell: $env:UMAMI_PASSWORD = 'your-password'");
    Console.WriteLine("  Bash:       export UMAMI_PASSWORD='your-password'");
    return;  // In CSX, 'return' at top level exits the script
}

// Create a single HttpClient instance - never create multiple instances in a loop!
// BaseAddress means all subsequent requests can use relative URLs
var httpClient = new HttpClient { BaseAddress = new Uri(umamiPath) };

// === STEP 1: Authenticate ===
// PostAsJsonAsync automatically serializes our anonymous object to JSON
// and sets the Content-Type header to application/json
Console.WriteLine("Step 1: Logging in...");
var loginPayload = new { username, password };
var loginResponse = await httpClient.PostAsJsonAsync("/api/auth/login", loginPayload);

// Always check for errors before trying to read the response body
if (!loginResponse.IsSuccessStatusCode)
{
    Console.WriteLine($"Login failed: {loginResponse.StatusCode}");
    var error = await loginResponse.Content.ReadAsStringAsync();
    Console.WriteLine($"Error body: {error}");
    return;
}

Console.WriteLine("Login successful!");

// === STEP 2: Extract JWT Token ===
// Use JsonDocument for one-off JSON parsing without creating dedicated DTOs
// This is perfect for exploratory testing when we don't know the exact schema
var loginContent = await loginResponse.Content.ReadAsStringAsync();
var loginJson = JsonDocument.Parse(loginContent);
var token = loginJson.RootElement.GetProperty("token").GetString();

// Add the JWT token to all future requests via the Authorization header
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

// === STEP 3: Build the API Request ===
// Always use UTC for API calls to avoid timezone confusion
var now = DateTime.UtcNow;
var yesterday = now.AddHours(-24);
var nowMs = ((DateTimeOffset)now).ToUnixTimeMilliseconds();
var yesterdayMs = ((DateTimeOffset)yesterday).ToUnixTimeMilliseconds();

var testUrl = $"/api/websites/{websiteId}/metrics?startAt={yesterdayMs}&endAt={nowMs}&type=url&unit=day&limit=10";

Console.WriteLine($"\nStep 2: Testing metrics endpoint...");
Console.WriteLine($"URL: {testUrl}");

// === STEP 4: Make the Request ===
var response = await httpClient.GetAsync(testUrl);
Console.WriteLine($"Status: {response.StatusCode}");

// Pretty-print the JSON response so we can understand the structure
var responseBody = await response.Content.ReadAsStringAsync();
try
{
    var formatted = JsonSerializer.Serialize(
        JsonSerializer.Deserialize<JsonElement>(responseBody),
        new JsonSerializerOptions { WriteIndented = true });
    Console.WriteLine($"Response:\n{formatted}");
}
catch
{
    // If it's not valid JSON, just print raw
    Console.WriteLine($"Response (raw):\n{responseBody}");
}

// What I learned from this script:
// 1. The API returns an array of objects with 'x' (url) and 'y' (count) properties
// 2. Empty results return [] not null
// 3. The JWT token expires after 24 hours

Testen met Afhankelijkheidsinjectie

Het probleem: Ik heb een NuGet pakket (Umami.Net) gepubliceerd en wil het precies testen zoals een consument het zou gebruiken - met een juiste afhankelijkheidsinjectie setup, niet door klassen direct te instantiëren.

Waarom CSX? Het maken van een testconsole project, het toevoegen van mijn NuGet referentie, het schrijven van alle DI boilerplate - dat is 15 + minuten ceremonie. Met CSX, Ik kan controleren of de consument ervaring in minder dan 2 minuten.

#!/usr/bin/env dotnet-script

// Reference my published NuGet package - this tests the ACTUAL PUBLISHED VERSION,
// not my local source code. This is crucial for verifying releases work correctly!
#r "nuget: Umami.Net, 0.1.0"

// Standard Microsoft DI packages - the same ones ASP.NET Core uses
#r "nuget: Microsoft.Extensions.DependencyInjection, 9.0.0"
#r "nuget: Microsoft.Extensions.Logging.Console, 9.0.0"

using Umami.Net;
using Umami.Net.UmamiData;
using Umami.Net.UmamiData.Models.RequestObjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Configuration
var websiteId = "32c2aa31-b1ac-44c0-b8f3-ff1f50403bee";
var umamiPath = "https://umami.mostlylucid.net";
var password = Environment.GetEnvironmentVariable("UMAMI_PASSWORD") ?? "";

if (string.IsNullOrEmpty(password))
{
    Console.WriteLine("ERROR: Set UMAMI_PASSWORD environment variable");
    return;
}

// === BUILD THE DI CONTAINER ===
// This mimics exactly what happens in a real ASP.NET Core app's Program.cs

var services = new ServiceCollection();

// Add logging so we can see what the library is doing internally
// Debug level will show HTTP requests, retries, token refreshes, etc.
services.AddLogging(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Debug);  // Show everything
});

// This is my library's extension method - this is the public API that users call
// I want to verify this works correctly without any hidden dependencies
services.AddUmamiData(umamiPath, websiteId);

// Build the container and resolve our service
var serviceProvider = services.BuildServiceProvider();
var umamiDataService = serviceProvider.GetRequiredService<UmamiDataService>();

Console.WriteLine("=== Testing Umami.Net Package via DI ===\n");

// === TEST THE LOGIN FLOW ===
Console.WriteLine("Testing login...");
var loginSuccess = await umamiDataService.LoginAsync("admin", password);
if (!loginSuccess)
{
    Console.WriteLine("ERROR: Login failed - check credentials");
    return;
}
Console.WriteLine("Login successful!\n");

// === TEST THE METRICS API ===
Console.WriteLine("Testing metrics API...");
var metricsResult = await umamiDataService.GetMetrics(new MetricsRequest
{
    StartAtDate = DateTime.UtcNow.AddHours(-24),
    EndAtDate = DateTime.UtcNow,
    Type = MetricType.url,   // Get URL metrics (most visited pages)
    Unit = Unit.day,
    Limit = 10
});

// Display results
Console.WriteLine($"API returned status: {metricsResult?.Status}");
if (metricsResult?.Data?.Length > 0)
{
    Console.WriteLine($"\nTop {Math.Min(5, metricsResult.Data.Length)} URLs in the last 24 hours:");
    foreach (var metric in metricsResult.Data.Take(5))
    {
        // metric.x = the URL path, metric.y = the view count
        Console.WriteLine($"  {metric.y,5} views - {metric.x}");
    }
}
else
{
    Console.WriteLine("No data returned - check date range or website ID");
}

// What I verified with this script:
// 1. The NuGet package installs correctly
// 2. The DI registration extension method works
// 3. The service can be resolved from the container
// 4. Login and API calls work as expected

Qdrant Vector Database testen

Het probleem: Ik integreer een Qdrant vector database voor semantische zoekopdracht. Voordat ik de productiedienst schrijf, moet ik begrijpen hoe de gRPC client werkt, hoe de API eruit ziet, en controleren of mijn lokale Qdrant instantie correct draait.

Waarom CSX? Vector databases zijn nieuw terrein voor veel ontwikkelaars. CSX laat me interactief experimenteren, verschillende operaties proberen en onmiddellijke resultaten zien voordat ik een architectuur begin.

#!/usr/bin/env dotnet-script

// Qdrant.Client is the official .NET client for the Qdrant vector database
#r "nuget: Qdrant.Client, 1.12.0"

using Qdrant.Client;
using Qdrant.Client.Grpc;

// === CRITICAL: Windows gRPC HTTP/2 Fix ===
// By default, .NET on Windows doesn't allow unencrypted HTTP/2 connections (used by gRPC)
// Without this line, you'll get cryptic "Protocol error" exceptions
// This must be called BEFORE creating the QdrantClient!
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

// Connect to Qdrant running locally
// Note: Port 6334 is gRPC (faster), port 6333 is REST API
// The .NET client uses gRPC for better performance
var client = new QdrantClient("localhost", 6334);

Console.WriteLine("=== Qdrant Vector Database Testing ===\n");

// === STEP 1: List Existing Collections ===
// A "collection" in Qdrant is like a table - it holds vectors with the same dimensionality
Console.WriteLine("Step 1: Checking existing collections...");
var collections = await client.ListCollectionsAsync();

if (!collections.Any())
{
    Console.WriteLine("No collections found. This is a fresh Qdrant instance.\n");
}
else
{
    foreach (var collection in collections)
    {
        var info = await client.GetCollectionInfoAsync(collection);
        Console.WriteLine($"  Collection: {collection}");
        Console.WriteLine($"    Points (vectors): {info.PointsCount}");
        Console.WriteLine($"    Status: {info.Status}");
    }
    Console.WriteLine();
}

// === STEP 2: Create a Test Collection ===
// Vector databases store "points" - each point has a vector and optional metadata (payload)
var testCollection = "csx_demo";

Console.WriteLine($"Step 2: Creating test collection '{testCollection}'...");
try
{
    await client.CreateCollectionAsync(
        collectionName: testCollection,
        vectorsConfig: new VectorParams
        {
            // Vector size MUST match your embedding model!
            // all-MiniLM-L6-v2 produces 384-dimensional vectors
            // text-embedding-ada-002 produces 1536-dimensional vectors
            Size = 384,

            // Cosine similarity is standard for text embeddings
            // Alternatives: Distance.Dot (dot product), Distance.Euclid (euclidean)
            Distance = Distance.Cosine
        });
    Console.WriteLine("Collection created successfully!\n");
}
catch (Exception ex) when (ex.Message.Contains("already exists"))
{
    Console.WriteLine("Collection already exists, continuing...\n");
}

// === STEP 3: Insert Test Data ===
// In production, vectors come from an embedding model (BERT, OpenAI, etc.)
// For testing, we'll use random vectors
Console.WriteLine("Step 3: Inserting test point...");

var testVector = Enumerable.Range(0, 384)
    .Select(_ => (float)Random.Shared.NextDouble())
    .ToArray();

// Payload = metadata attached to the vector
// This is what you filter on and return in search results
var payload = new Dictionary<string, Value>
{
    ["title"] = "Understanding Vector Databases",
    ["slug"] = "understanding-vector-databases",
    ["language"] = "en",
    ["created"] = DateTime.UtcNow.ToString("O")
};

await client.UpsertAsync(
    collectionName: testCollection,
    points: new[]
    {
        new PointStruct
        {
            Id = Guid.NewGuid(),  // Unique identifier for this point
            Vectors = testVector,
            Payload = { payload }
        }
    });
Console.WriteLine("Point inserted!\n");

// === STEP 4: Search for Similar Vectors ===
// In production, you'd embed a search query and find similar documents
Console.WriteLine("Step 4: Searching for similar vectors...");

var searchVector = Enumerable.Range(0, 384)
    .Select(_ => (float)Random.Shared.NextDouble())
    .ToArray();

var results = await client.SearchAsync(
    collectionName: testCollection,
    vector: searchVector,
    limit: 5,
    scoreThreshold: 0.0f  // Return all results (random vectors won't have high similarity)
);

Console.WriteLine($"Found {results.Count} results:");
foreach (var result in results)
{
    // Score: 0 to 1 for cosine similarity (higher = more similar)
    Console.WriteLine($"  Score: {result.Score:F4}");
    Console.WriteLine($"    Title: {result.Payload["title"].StringValue}");
    Console.WriteLine($"    Slug: {result.Payload["slug"].StringValue}");
}

// === STEP 5: Clean Up ===
Console.WriteLine($"\nStep 5: Deleting test collection...");
await client.DeleteCollectionAsync(testCollection);
Console.WriteLine("Done! Test collection cleaned up.");

// What I learned from this script:
// 1. The gRPC client is fast but needs the HTTP/2 switch on Windows
// 2. Collection creation requires specifying vector dimensions upfront
// 3. Payloads can be arbitrary key-value pairs
// 4. Search returns results sorted by similarity score

Meer praktische voorbeelden

Een HTTP-eindpunt testen

#r "nuget: System.Net.Http.Json, 9.0.0"

using System.Net.Http.Json;

var http = new HttpClient();
http.DefaultRequestHeaders.Add("User-Agent", "CSX-Test");

// Test a GET endpoint
var response = await http.GetFromJsonAsync<JsonElement>(
    "https://api.github.com/repos/dotnet/runtime");

Console.WriteLine($"Stars: {response.GetProperty("stargazers_count")}");
Console.WriteLine($"Forks: {response.GetProperty("forks_count")}");

JSON-serialisatie testen

#r "nuget: System.Text.Json, 8.0.0"

using System.Text.Json;
using System.Text.Json.Serialization;

public record Person(
    string Name,
    int Age,
    [property: JsonPropertyName("email_address")] string Email);

var person = new Person("Scott", 50, "[email protected]");

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var json = JsonSerializer.Serialize(person, options);
Console.WriteLine(json);

// Deserialize back
var parsed = JsonSerializer.Deserialize<Person>(json, options);
Console.WriteLine($"Parsed: {parsed}");

Database-zoekopdrachten testen

#r "nuget: Npgsql, 8.0.0"
#r "nuget: Dapper, 2.1.24"

using Npgsql;
using Dapper;

var connectionString = "Host=localhost;Database=test;Username=postgres;Password=secret";

await using var conn = new NpgsqlConnection(connectionString);

// Quick query test
var results = await conn.QueryAsync<dynamic>(
    "SELECT * FROM users WHERE created_at > @date",
    new { date = DateTime.UtcNow.AddDays(-7) });

foreach (var row in results)
{
    Console.WriteLine($"{row.id}: {row.name}");
}

Regex-patronen testen

using System.Text.RegularExpressions;

var patterns = new[]
{
    @"^\d{4}-\d{2}-\d{2}$",           // Date
    @"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", // Email
    @"^https?://[\w\-]+(\.[\w\-]+)+", // URL
};

var testCases = new[]
{
    "2025-11-24",
    "[email protected]",
    "https://mostlylucid.net",
    "not-a-date",
    "invalid-email",
};

foreach (var test in testCases)
{
    Console.WriteLine($"\n{test}:");
    foreach (var pattern in patterns)
    {
        var match = Regex.IsMatch(test, pattern);
        if (match) Console.WriteLine($"  ✓ Matches: {pattern}");
    }
}

Testen van LINQ-vragen

var data = new[]
{
    new { Name = "Alice", Age = 30, Department = "Engineering" },
    new { Name = "Bob", Age = 25, Department = "Marketing" },
    new { Name = "Charlie", Age = 35, Department = "Engineering" },
    new { Name = "Diana", Age = 28, Department = "Engineering" },
};

// Test complex LINQ query
var result = data
    .Where(x => x.Department == "Engineering")
    .GroupBy(x => x.Age >= 30)
    .Select(g => new
    {
        Senior = g.Key,
        Count = g.Count(),
        Names = string.Join(", ", g.Select(x => x.Name))
    });

foreach (var group in result)
{
    Console.WriteLine($"Senior: {group.Senior}, Count: {group.Count}, Names: {group.Names}");
}

Zoeken van Qdrant-vector testen

#r "nuget: Qdrant.Client, 1.12.0"

using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

// Test collection exists
var collections = await client.ListCollectionsAsync();
Console.WriteLine("Collections:");
foreach (var collection in collections)
{
    Console.WriteLine($"  - {collection}");
}

// Test a search (assuming you have embeddings)
var testVector = Enumerable.Range(0, 384).Select(_ => (float)Random.Shared.NextDouble()).ToArray();

try
{
    var results = await client.SearchAsync(
        collectionName: "blog_posts",
        vector: testVector,
        limit: 5);

    foreach (var result in results)
    {
        Console.WriteLine($"Score: {result.Score}, Id: {result.Id}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Search failed: {ex.Message}");
}

IDE-ondersteuning

Visual Studio Code

Installeer de C# Dev Kit uitbreiding. Je krijgt:

  • Syntaxismarkering
  • IntelliSense
  • Run/Debug via codeLens

Aanmaken .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Run CSX",
            "type": "coreclr",
            "request": "launch",
            "program": "dotnet",
            "args": ["script", "${file}"],
            "cwd": "${workspaceFolder}"
        }
    ]
}

JetBrains Rider

Rider heeft ingebouwde CSX ondersteuning. Klik met de rechtermuisknop op elke .csx bestand en selecteer "Rennen."

Tips en trucs

Gebruik de Shebang

Voeg een shebang toe om scripts direct uitvoerbaar te maken op Linux/Mac:

#!/usr/bin/env dotnet-script

Console.WriteLine("Runs directly with ./script.csx");

Argumenten met standaardwaarden

Command-line argumenten via de globale toegang Args variabele:

// run: dotnet script test.csx -- arg1 arg2 "arg with spaces"
Console.WriteLine($"Arguments: {Args.Count}");
foreach (var (arg, index) in Args.Select((a, i) => (a, i)))
{
    Console.WriteLine($"  [{index}]: {arg}");
}

// Common pattern: use args with defaults
var environment = Args.ElementAtOrDefault(0) ?? "development";
var verbose = Args.Contains("--verbose");

Console.WriteLine($"Environment: {environment}, Verbose: {verbose}");

Omgevingsvariabelen voor geheimen

Nooit hardcode geheimen - gebruik omgevingsvariabelen:

var apiKey = Environment.GetEnvironmentVariable("API_KEY");
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD");

if (string.IsNullOrEmpty(apiKey))
{
    Console.Error.WriteLine("ERROR: API_KEY not set");
    Console.Error.WriteLine("Run: $env:API_KEY='your-key' (PowerShell)");
    Console.Error.WriteLine(" or: export API_KEY='your-key' (bash)");
    Environment.Exit(1);
}

// Safely log partial key for debugging
Console.WriteLine($"Using API key: {apiKey[..4]}...{apiKey[^4..]}");

Interactieve modus (REPL)

Start een interactieve sessie voor verkenning:

dotnet script

Je krijgt een C# REPL:

> var x = 42;
> x * 2
84
> #r "nuget: Newtonsoft.Json, 13.0.3"
> using Newtonsoft.Json;
> JsonConvert.SerializeObject(new { foo = "bar" })
"{"foo":"bar"}"

Debuggen

Debug met VS-code door een breekpunt toe te voegen en te draaien met F5, of:

dotnet script test.csx --debug

Records gebruiken voor snelle DTO's

Geen klassebestanden nodig - definieer inline:

// Records are perfect for CSX - single line definitions
public record Person(string Name, int Age, string Email);
public record ApiResponse<T>(bool Success, T? Data, string? Error);
public record SearchResult(string Title, string Slug, float Score);

var person = new Person("Scott", 50, "[email protected]");
var response = new ApiResponse<Person>(true, person, null);

Pretty Print met Dumpify

#r "nuget: Dumpify, 0.6.5"

using Dumpify;

var data = new
{
    Name = "Test",
    Items = new[] { 1, 2, 3 },
    Nested = new { Foo = "bar" }
};

data.Dump();  // Pretty console output with colors

Gemeenschappelijke onderwerpen & Gotcha's

Uitgave: "NuGet pakket niet gevonden"

Eerste run is traag - pakketten downloaden op de achtergrond:

#r "nuget: SomePackage, 1.0.0"  // First run: downloads
                                  // Second run: uses cache

Repareren: Wacht tot de eerste run voltooid is, of pre-download:

dotnet script init  # Creates omnisharp.json
dotnet script       # Downloads packages in REPL

Onderwerp: "Type of naamruimte niet gevonden"

Pakketversie kan verkeerd of onverenigbaar zijn:

// Bad - version doesn't have the type you need
#r "nuget: Microsoft.Extensions.Http, 6.0.0"

// Good - use matching version for your .NET SDK
#r "nuget: Microsoft.Extensions.Http, 9.0.0"

Onderwerp: gRPC op Windows

Qdrant en andere gRPC-services falen met HTTP/2-fouten:

// Add this BEFORE creating gRPC clients
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

var client = new QdrantClient("localhost", 6334);  // Now works

Uitgave: HttpClient Socket Uitputting

Maak geen meerdere HttpClient instanties aan in een lus:

// Bad - creates socket exhaustion
foreach (var url in urls)
{
    using var client = new HttpClient();  // DON'T do this
    await client.GetAsync(url);
}

// Good - reuse HttpClient
using var client = new HttpClient();
foreach (var url in urls)
{
    await client.GetAsync(url);
}

Uitgave: Async op topniveau

Top-level async werkt gewoon in CSX - geen async Main nodig:

// This works - no async Main needed
var response = await httpClient.GetAsync("https://example.com");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);

Issue: Assembly Load Conflicts

Bij verwijzing naar lokale DLL's die afhankelijkheden hebben:

// Order matters - load dependencies first
#r "Mostlylucid.Shared/bin/Debug/net9.0/Mostlylucid.Shared.dll"
#r "Mostlylucid.Services/bin/Debug/net9.0/Mostlylucid.Services.dll"

// Or use NuGet for dependencies, local for your code
#r "nuget: Microsoft.Extensions.Logging, 9.0.0"
#r "MyLibrary/bin/Debug/net9.0/MyLibrary.dll"

Onderwerp: Script wordt niet uitgevoerd na bewerken

IntelliSense cache kan oud worden:

# Clear the cache
rm -rf ~/.dotnet-script/          # Linux/Mac
rd /s /q %USERPROFILE%\.dotnet-script\  # Windows

Uitgave: Nullable Reference Types

CSX maakt gebruik van verschillende standaards - activeer expliciet indien nodig:

#nullable enable

string? nullableString = null;  // OK
string nonNullable = null;      // Warning

Wanneer CSX versus volledig project gebruiken

CSX gebruiken wanneer:

  • Snelle eenmalige tests
  • API-verkenning
  • Prototyperingsalgoritmen
  • NuGet-pakketten testen alvorens toe te voegen aan project
  • Valideren van regex, LINQ, JSON-serialisatie
  • Database query testen
  • Leren/experimenteren

Een volledig project gebruiken wanneer:

  • Meerdere bestanden met complexe afhankelijkheden
  • Eenheidstest (gebruik xEenheid/NEenheid)
  • Productiecode
  • Samenwerking met het team
  • CI/CD-pijpleidingen

Real-World Voorbeeld: Mijn Blog API testen

Hier is een script dat ik gebruik om het meest lucide zoekeindpunt te testen:

#r "nuget: System.Net.Http.Json, 8.0.0"

using System.Net.Http.Json;

var baseUrl = Args.Length > 0 ? Args[0] : "https://www.mostlylucid.net";
var searchTerm = Args.Length > 1 ? Args[1] : "docker";

var http = new HttpClient { BaseAddress = new Uri(baseUrl) };

Console.WriteLine($"Searching {baseUrl} for '{searchTerm}'...\n");

var results = await http.GetFromJsonAsync<JsonElement>(
    $"/api/search?term={Uri.EscapeDataString(searchTerm)}");

if (results.TryGetProperty("results", out var items))
{
    foreach (var item in items.EnumerateArray().Take(5))
    {
        var title = item.GetProperty("title").GetString();
        var slug = item.GetProperty("slug").GetString();
        Console.WriteLine($"- {title}");
        Console.WriteLine($"  /{slug}\n");
    }
}

Voer het uit:

dotnet script search-test.csx -- https://localhost:5001 "entity framework"

Samenvatting

CSX scripts zijn de perfecte middenweg tussen de C# REPL en een volledig project. Ze zijn ideaal voor:

  • Snelheid: Schrijven en draaien in seconden
  • Eenvoud: Geen projectceremonie
  • Vermogen: Volledige C# met NuGet ondersteuning
  • Overdraagbaarheid: Een enkel bestand delen

Volgende keer moet je iets snel testen in C#, skip dotnet new console en reiken naar dotnet script In plaats daarvan.

Middelen:

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.