Back to "Använda CSX-skript för snabb C#-testning"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

C# dotnet-script Scripting Testing

Använda CSX-skript för snabb C#-testning

Wednesday, 26 November 2025

Behöver du testa en kopia av C#-koden utan att snurra upp ett komplett projekt? C# Script-filer (.csx) Låt dig skriva och köra C#-kod som ett skriptspråk. Nej Program.cs, , . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .csproj, ingen byggsteg - bara skriva och köra. Perfekt för att testa API:er, validera logik, eller prototyper innan du förbinder dig till en fullständig implementering.

För jag rullar ut hela semanti c sökfunktion Jag tänkte att jag skulle dela hur jag använder .csx Filer för ad hoc-tester i detta och andra projekt.

CSX vs .NET 10 filbaserade appar

Innan du dyker in, låt oss tala till elefanten i rummet: .NET 10 har nu infödda "fil-baserade appar" som låter dig köra .cs filer direkt med dotnet run app.cs. Hur kan detta jämföras med CSX?

.NET 10 Filbaserade appar (nu tillgängliga!)

Med .NET 10 kan du köra enfil C# direkt:

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

Kännetecken:

  • Native till SDK - inga extra verktyg behövs
  • Använder bekant .cs förlängning
  • NuFå referenser via #:package Direktiv
  • Fullt avlusarstöd från dag ett
  • Samma kompilator som vanliga projekt
// app.cs - .NET 10 style
#:package [email protected]

using Newtonsoft.Json;

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

CSX-skript (finns nu)

CSX via dotnet-script har funnits sedan 2017:

# Available today
dotnet script app.csx

Kännetecken:

  • Fungerar med NET 6, 7, 8, 9, 10
  • Rikt ekosystem och verktyg
  • REPL-läge för interaktiv prospektering
  • Bevisade och stridsprövade
// 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));

Vad bör du använda?

med funktionen CSX (dotnet-script) .NET 10 File Apps |---------|---------------------|-------------------| | Tillgänglighet .NET 6+ till .NET 10 | Anläggning | dotnet tool install -g dotnet-script och byggd i SDK | Filförlängning | .csx | .cs | | Nu Hämta syntax | #r "nuget: Pkg, Ver" | #:package Pkg@Ver | | REPL- läge - Ja, inte än. | IDE-stöd Bra (VS-kod, ryttare) | Felsökning Ja Ja (inhemsk)

Min rekommendation:

  • Prova .NET 10 filappar först - Inhemskt stöd innebär mindre verktyg overhead
  • Tillbaka till CSX om du behöver REPL-läge eller är på en äldre .NET-version
  • Begreppen är nästan identiska - kunskapsöverföringar lätt mellan båda

Resten av denna artikel täcker CSX som fortfarande fungerar bra och har vissa funktioner (som REPL) som .NET 10 filappar inte har ännu.

Vad är CSX?

CSX (C# Script) filer är C#- kodfiler som kan köras direkt utan sammanställning i ett projekt. Se det som "Python- stil" C# - du skriver kod, du kör den, du ser resultat.

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

Kör den:

dotnet script hello.csx

Så där ja. Main() metod, ingen namnrymd, inget klassomslag krävs.

Installerar dotnet- script

Det mest populära sättet att köra CSX-filer är via dotnet- script:

dotnet tool install -g dotnet-script

Kontrollera installationen:

dotnet script --version

Var CSX passar i testcykeln

Innan du dyker in i "hur", låt oss förstå "när". CSX-skript upptar en unik plats i testpyramiden:

                    ┌─────────────────┐
                    │   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-skript som "Mini integrationstest"

CSX skript är inte en ersättning för formella tester - de är en komplement. Tänk på dem som:

  • Gränser för genomförande före genomförande - Verifiera ett API fungerar innan du bygger en tjänst runt det
  • Felsökningshjälpmedel - Isolera och reproducera problem utan att bygga upp hela din app
  • Förberedande tester - Förstå hur ett bibliotek beter sig innan man skriver enhetsprov
  • Rökprov - Snabb sundhet kontroller mot verkliga tjänster (databas, API:er, köer)

Utvecklingsarbetsflödet

Här är hur CSX passar in i en typisk funktionsutvecklingscykel:

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

Verkligt exempel: Att bygga Umami-integrationen

När jag byggde Umami analytics integration för denna blogg, var mitt arbetsflöde:

  1. CSX: Testa det råa API:et - Hur ser svaren ut?
  2. CSX: Omvandling av testtider - Hittade en bugg här innan du skriver någon produktionskod!
  3. Implementera: Bygg UmamiClient - Med självförtroende eftersom jag redan hade validerat API:et
  4. xUnit: Skriv enhetstester - Mock HttpClient, test serialiseringslogik
  5. CSX: Felsökningsproduktionsfråga Manus för att isolera problemet.

CSX-skripten ersatte inte mina enhetstest - de förhindrade mig från att skriva kod som inte skulle fungera och hjälpte mig Felsökningsproblem snabbare när de inträffade.

Varför använda CSX för testning?

1. Noll ceremoni

Traditionell metod för att testa ett API-anrop:

  1. Skapa nytt konsolprojekt
  2. Lägg till NuGet- paket
  3. Skriv Program.cs
  4. Bygg
  5. Kör
  6. Ta bort projekt när det är gjort

CSX- inflygning:

  1. Skriv skript
  2. Kör skript

2. Inline NuFå referenser

Behöver du ett paket? Referera det direkt i ditt manus:

#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));

Första kör nedladdningar paket. Efterföljande körningar använder cache.

3. Referens lokala DLL-filer

Testa ditt eget bibliotek? Referera det direkt:

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

using MyLibrary;

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

4. Referens Andra skript

Dela komplexa skript till återanvändbara delar:

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

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

Verkliga exempel från det här projektet

Dessa är inte påhittade exempel - de är verkliga manus jag använder för att felsöka och testa bloggens kodbas. Var och en löste ett verkligt problem jag stött på under utvecklingen.

Pröva API Timestamps

Problemet: Min Umami analys integration var att returnera tomma data. Efter timmar av felsökning, misstänkte jag att tidsstämpeln konverteringen var fel - Umami API förväntar sig Unix tidsstämpels i millisekunder, men jag var inte säker på om min .NET-kod producerade rätt format.

Varför CSX? Jag kunde ha lagt till loggning till produktionskoden, återuppbyggt, utplacerat och kontrollerat loggar. Eller så kunde jag skriva ett snabbt manus för att verifiera min hypotes på 30 sekunder.

#!/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.

Resultatet: Det här skriptet visade att tidsstämpeln var 1 år i framtiden. Jag spårade felet tillbaka till att använda DateTime.Now istället för DateTime.UtcNow Fast på 5 minuter istället för 5 timmars felsökning.

Test av frågesträngsgenerering

Problemet: Jag behövde verifiera att ASP.NET's QueryHelpers klass genererar frågesträngar i exakt det format som Umami API förväntar sig. Är det URL-koda specialtecken? Vilken ordning är parametrarna i?

Varför CSX? Läsa dokumentation är en sak, men att se den faktiska utdata berättar exakt vad din kod kommer att producera.

#!/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

Testa råa HTTP- API- samtal

Problemet: Innan jag byggde en fullserviceklass med beroendeinjektion, felhantering, försökslogik och enhetstester ville jag verifiera att API:et faktiskt fungerar och förstå dess svarsformat.

Varför CSX? Det är snabbare att skriva 50 rader av undersökande kod än att bygga rätt serviceinfrastruktur. Om API:et inte fungerar som jag förväntar mig, har jag slösat 5 minuter i stället för 5 timmar.

#!/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

Testning med beroendeinjektion

Problemet: Jag har publicerat ett NuGet-paket (Umami.Net) och vill testa det exakt som en konsument skulle använda det - med rätt beroende injicering installation, inte genom att ögonblickliga klasser direkt.

Varför CSX? Skapa en testkonsol projekt, lägga till min NuGet referens, skriva alla DI pannplatta - det är 15 + minuter av ceremoni. Med CSX kan jag verifiera konsumenternas upplevelse på under 2 minuter.

#!/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

Test av Qdrants vektordatabas

Problemet: Jag integrerar en Qdrant vektordatabas för semantisk sökning. Innan jag skriver produktionstjänsten, måste jag förstå hur gRPC klienten fungerar, hur API:et ser ut, och verifiera att min lokala Qdrant instans fungerar korrekt.

Varför CSX? Vektordatabaser är nya områden för många utvecklare. CSX låter mig experimentera interaktivt, prova olika verksamheter och se omedelbara resultat innan jag förbinder mig till en arkitektur.

#!/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

Fler praktiska exempel

Testa en HTTP-slutpunkt

#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")}");

Pröva JSON serialisering

#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}");

Test av databasfrågor

#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}");
}

Pröva regexmönster

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}");
    }
}

Test av LINQ-frågor

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}");
}
#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-stöd

Visuell studiokod

Installera C# Dev Kit förlängning. Du får:

  • Syntaxfärgläggning
  • IntelliSense
  • Kör/debug via codeLens

Skapa .vscode/launch.json:

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

JetBrains Ryttare

Rider har inbyggt stöd för CSX. Högerklicka på valfri .csx fil och välj "Kör".

Tips och tricks

Använd Shebang

Lägg till en shebang för att göra skript direkt körbara på Linux/Mac:

#!/usr/bin/env dotnet-script

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

Argument med förval

Åtkomst till kommandoradsargument via den globala Args variabel:

// 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}");

Miljövariabler för hemligheter

Aldrig hårdkod hemligheter - använd miljövariabler:

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..]}");

Interaktivt läge (REPL)

Starta en interaktiv session för prospektering:

dotnet script

Du får en 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"}"

Felsökning

Felsökning med VS-kod genom att lägga till en brytpunkt och köra med F5, eller:

dotnet script test.csx --debug

Använd poster för snabba DTO:er

Inga klassfiler behövs - definiera 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);

Vackert tryck med 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

Vanliga frågor & Gotchas

Frågan: "NuGet-paketet hittades inte"

Första körningen är långsam - paket nedladdning i bakgrunden:

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

Rätta: Vänta på första körningen för att slutföra, eller för nedladdning:

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

Utfärdande: "Typ eller namnrymd hittades inte"

Paketversion kan vara fel eller inkompatibel:

// 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"

Frågan: gRPC på Windows

Qdrant och andra gRPC-tjänster misslyckas med HTTP/2-fel:

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

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

Utfärdande: HttpClient Socket Utmattning

Skapa inte flera HtttpClient- instanser i en loop:

// 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);
}

Frågan: Asynkronisera på toppnivå

Async på toppnivå fungerar bara i CSX - ingen async Main behövs:

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

Fråga: Sammankomster Ladda konflikter

När referenser lokala DLLer som har beroenden:

// 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"

Utfärda: Skript körs inte efter redigering

IntelliSense cache kan bli gammal:

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

Utfärdande: Nullable Reference Types

CSX använder olika standardvärden - aktivera explicit om det behövs:

#nullable enable

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

När du ska använda CSX vs full projekt

Använd CSX när:

  • Snabba engångstester
  • Utforskning av API
  • Prototypningsalgoritmer
  • Testa NuGet-paket innan du lägger till i projektet
  • Validerar regex, LINQ, JSON serialisering
  • Test av databasförfrågan
  • Inlärning/experiment

Använd ett fullständigt projekt när:

  • Flera filer med komplexa beroenden
  • Enhetstestning (använd xUnit/NUnit)
  • Tillverkningskod
  • Samarbete mellan team
  • Rörledningar för CI/CD

Real-World Exempel: Testa min blogg API

Här är ett skript som jag använder för att testa Sökslutpunkten Mostlylucid:

#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");
    }
}

Kör den:

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

Sammanfattning

CSX-skript är den perfekta medelpunkten mellan C# REPL och ett komplett projekt. De är idealiska för:

  • Varvtal: Skriv och kör på några sekunder
  • Enkelhet: Ingen projektceremoni
  • Effekt: Full C# med stöd för NuGet
  • Anpassningsförmåga: Dela en enda fil

Nästa gång du behöver testa något snabbt i C#, hoppa dotnet new console och sträva efter dotnet script istället.

Resurser:

logo

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