Utilizzo degli script CSX per test rapidi C# (Italiano (Italian))

Utilizzo degli script CSX per test rapidi C#

Wednesday, 26 November 2025

//

29 minute read

Hai bisogno di testare un frammento di codice C# senza ruotare un progetto completo? File script C# (.csx) ti permette di scrivere ed eseguire il codice C# come un linguaggio di scripting. No Program.cs, no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no , no .csproj, no build step - basta scrivere ed eseguire. Perfetto per testare le API, convalidare la logica, o la prototipazione prima di impegnarsi in una piena implementazione.

Before tiro fuori l'intera funzionalità di ricerca semanti c Ho pensato di condividere come uso .csx file per test ad hoc in questo e in altri progetti.

CSX vs .NET 10 App basate su file

Prima di tuffarsi, indirizziamo l'elefante nella stanza: .NET 10 ora ha nativo "file-based apps" che consente di eseguire .cs file direttamente con dotnet run app.cs. Come si confronta questo con CSX?

.NET 10 applicazioni basate su file (ora disponibile!)

Con .NET 10, è possibile eseguire un singolo file C# direttamente:

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

Caratteristiche:

  • Originario dell'SDK - non sono necessari strumenti aggiuntivi
  • Usa familiarità .cs estensione
  • NuGet referenze via #:package direttiva
  • Supporto completo del debug dal primo giorno
  • Stesso compilatore dei progetti regolari
// app.cs - .NET 10 style
#:package [email protected]

using Newtonsoft.Json;

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

Script CSX (Disponibile ora)

CSX via dotnet-script è in giro dal 2017:

# Available today
dotnet script app.csx

Caratteristiche:

  • Funziona con .NET 6, 7, 8, 9, 10
  • Ricco ecosistema e strumenti
  • Modalità REPL per l'esplorazione interattiva
  • Prove e prove di battaglia
// 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));

Quale dovresti usare?

Caratteristica CSX (dotnet-script) .NET 10 applicazioni di file
Disponibilità .NET 6+ .NET 10
Installazione dotnet tool install -g dotnet-script Costruito in SDK
Estensione del file .csx .cs
Sintassi di NuGet #r "nuget: Pkg, Ver" #:package Pkg@Ver
Modalità REPL Non ancora
Supporto IDE Good (VS Code, Rider) Improvement
Debug Sì (nativo)

La mia raccomandazione:

  • Provare .NET 10 applicazioni di file prima - supporto nativo significa meno utensili in testa
  • Ripiegare a CSX se avete bisogno di modalità REPL o sono su una vecchia versione .NET
  • I concetti sono quasi identici - trasferimento di conoscenze facilmente tra entrambi

Il resto di questo articolo copre CSX che funziona ancora grande e ha alcune caratteristiche (come REPL) che .NET 10 applicazioni file non hanno ancora.

Che cos'è CSX?

I file CSX (C# Script) sono file di codice C# che possono essere eseguiti direttamente senza compilazione in un progetto. Consideralo come "Python-style" C# - scrivi codice, lo esegui, vedi risultati.

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

Eseguilo:

dotnet script hello.csx

No, no, no, no, no, no, no, no, no, no, no, no, no. Main() metodo, nessun namespace, nessun wrapper di classe richiesto.

Installazione dello script dotnet

Il modo più popolare per eseguire i file CSX è attraverso dotnet-script:

dotnet tool install -g dotnet-script

Verificare l'installazione:

dotnet script --version

Dove CSX si adatta al ciclo di prova

Prima di tuffarsi nel "come," capiamo il "quando." Gli script CSX occupano un punto unico nella piramide di test:

                    ┌─────────────────┐
                    │   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)   │
              ─┴─────────────────────────┴─

Script CSX come "Mini Integration Tests"

Gli script CSX non sono un sostituto per i test formali - sono un complemento. Pensa a loro come:

  • picchi di pre-attuazione - Verificare un API funziona prima di costruire un servizio intorno ad esso
  • Aiuti per il debug - Isolare e riprodurre i problemi senza ricostruire l'intera app
  • Test esplorativi - Capire come si comporta una libreria prima di scrivere i test di unità
  • Test del fumo - Quick sanity contro i servizi reali (database, API, code)

Il flusso di lavoro per lo sviluppo

Ecco come CSX si inserisce in un tipico ciclo di sviluppo di funzionalità:

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

Esempio reale: costruire l'integrazione Umami

Quando ho costruito l'integrazione di Umami Analytics per questo blog, il mio flusso di lavoro era:

  1. CSX: testare l'API cruda - Funziona l'autenticazione?
  2. CSX: Conversione timestamp di test - Ho trovato un bug qui prima di scrivere qualsiasi codice di produzione!
  3. Attua: Costruisci UmamiClient - Con fiducia perché avevo già convalidato l'API
  4. xUnit: Scrivere i test delle unità - Mock HttpClient, logica di serializzazione test
  5. CSX: problema di produzione di debug - Metrics ritorno vuoto? Script per isolare il problema

Gli script CSX non hanno sostituito i miei test unitari. mi ha impedito di scrivere codice che non avrebbe funzionato e mi ha aiutato debug più veloce quando si sono verificati.

Perché utilizzare CSX per il testing?

1. Cerimonia zero

Approccio tradizionale per testare una chiamata API:

  1. Crea un nuovo progetto di console
  2. Aggiungi pacchetti NuGet
  3. Scrivi Program.cs
  4. Genera
  5. Esegui
  6. Elimina progetto alla fine

Approccio CSX:

  1. Scrivi script
  2. Esegui script

2. Riferimenti NuGet Inline

Hai bisogno di un pacchetto? Fai riferimento direttamente nel tuo 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));

Prima eseguire i pacchetti di download. Successivo esegue utilizzare la cache.

3. DLL locali di riferimento

Testare la propria libreria? Reference it directly:

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

using MyLibrary;

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

4. Riferimento Altri script

Dividi script complessi in parti riutilizzabili:

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

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

Esempi reali di questo progetto

Questi non sono esempi inventati - sono script reali che uso per debug e testare la base di codice di questo blog. Ognuno ha risolto un problema reale che ho incontrato durante lo sviluppo.

Verifica dei timestamp delle API

Il problema: Dopo ore di debug, ho sospettato che la conversione timestamp fosse sbagliata - l'API Umami si aspetta che i timestamp Unix in millisecondi, ma non ero sicuro che il mio codice .NET stesse producendo il formato corretto.

Perche' CSX? Avrei potuto aggiungere il log al codice di produzione, ricostruire, distribuire e controllare i log. Oppure avrei potuto scrivere uno script veloce per verificare la mia ipotesi in 30 secondi.

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

Il risultato: Questo script ha dimostrato che il timestamp è stato 1 anno in futuro. Ho rintracciato il bug di nuovo a utilizzare DateTime.Now invece di DateTime.UtcNow nel codice di produzione. Fisso in 5 minuti invece di potenzialmente 5 ore di debug.

Verifica della generazione delle stringhe di query

Il problema: Avevo bisogno di verificare che ASP.NET QueryHelpers class genera stringhe di query nel formato esatto che l'API Umami si aspetta. Codifica caratteri speciali con URL? In che ordine sono i parametri?

Perche' CSX? Leggere la documentazione è una cosa, ma vedere l'output effettivo ti dice esattamente ciò che il tuo codice produrrà.

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

Verifica delle chiamate API HTTP Raw

Il problema: Prima di costruire una classe di servizio completa con iniezione di dipendenza, gestione degli errori, riprova logica, e test unitari, ho voluto verificare che l'API funziona effettivamente e capire il suo formato di risposta.

Perche' CSX? È più veloce scrivere 50 righe di codice esplorativo che costruire un'infrastruttura di servizio adeguata. Se l'API non funziona come mi aspetto, ho sprecato 5 minuti invece di 5 ore.

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

Test con iniezione di dipendenza

Il problema: Ho pubblicato un pacchetto NuGet (Umami.Net) e voglio testarlo esattamente come un consumatore lo userebbe - con una corretta configurazione di iniezione di dipendenza, non istantanee le classi direttamente.

Perche' CSX? Creando un progetto di console di prova, aggiungendo il mio riferimento NuGet, scrivendo tutta la piastra di caldaia DI - che è 15+ minuti di cerimonia. Con CSX, posso verificare l'esperienza del consumatore in meno di 2 minuti.

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

Verifica della banca dati vettoriale Qdrant

Il problema: Sto integrando un database vettoriale Qdrant per la ricerca semantica. Prima di scrivere il servizio di produzione, devo capire come funziona il client GRPC, come appare l'API e verificare che la mia istanza Qdrant locale sia in esecuzione correttamente.

Perche' CSX? I database vettoriali sono un nuovo territorio per molti sviluppatori. CSX mi permette di sperimentare interattivamente, provando diverse operazioni e vedendo risultati immediati prima di impegnarsi in un'architettura.

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

Esempi più pratici

Provare un punto finale HTTP

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

Prova di serializzazione JSON

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

Testing Database Queries

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

Provare i modelli Regex

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

Verifica delle domande LINQ

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

Verificare la ricerca vettoriale Qdrant

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

Supporto IDE

Visual Studio Code

Installare il C# Dev Kit estensione. Ottieni:

  • Evidenziazione sintassi
  • IntelliSense
  • Esegui/Debug tramite CodeLens

Crea .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 ha integrato il supporto CSX. Fare clic con il tasto destro su qualsiasi .csx file e selezionare "Esegui."

Suggerimenti e trucchi

Usa il Shebang

Aggiungi uno shebang per rendere gli script eseguibili direttamente su Linux/Mac:

#!/usr/bin/env dotnet-script

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

Argomenti con predefiniti

Accedere agli argomenti della riga di comando tramite il globale Args variabile:

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

Variabili di ambiente per segreti

Mai segreti hardcode - utilizzare variabili d'ambiente:

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

Modalità interattiva (REPL)

Avviare una sessione interattiva per l'esplorazione:

dotnet script

Si ottiene un 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"}"

Debug

Debug con codice VS aggiungendo un punto di interruzione ed eseguendo con F5, oppure:

dotnet script test.csx --debug

Usa record per DTO veloci

Nessun file di classe necessario - definisci 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);

Stampa abbastanza con 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

Problemi comuni & Gotchas

Numero: "NuGet pacchetto non trovato"

La prima esecuzione è lenta - i pacchetti vengono scaricati in background:

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

Correggi: Attendere il primo avvio per completare, o pre-download:

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

Numero: "Tipo o spazio dei nomi non trovato"

La versione del pacchetto potrebbe essere errata o incompatibile:

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

Numero: GRPC su Windows

Qdrant e altri servizi gRPC falliscono con errori HTTP/2:

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

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

Numero: Esaurimento del socket HttpClient

Non creare più istanze HttpClient in un 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);
}

Numero: Async at Top Level

Async top-level funziona solo in CSX - nessun bisogno di async Main:

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

Problema: Conflitti di carico dell'assemblea

Quando si fa riferimento alle DLL locali che hanno dipendenze:

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

Numero: lo script non eseguirà dopo la modifica

IntelliSense cache può ottenere stantio:

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

Numero: Tipologie di riferimento nullabili

CSX utilizza diverse impostazioni predefinite - abilita esplicitamente se necessario:

#nullable enable

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

Quando usare CSX vs progetto completo

Usare CSX quando:

  • Veloci test una tantum
  • Esplorazione delle API
  • Algoritmi di prototipazione
  • Testare i pacchetti NuGet prima di aggiungere al progetto
  • Convalida della serializzazione regex, LINQ, JSON
  • Verifica delle query dei database
  • Apprendimento/sperimentazione

Usare un progetto completo quando:

  • File multipli con dipendenze complesse
  • Prova dell'unità (usare xUnit/NUnit)
  • Codice di produzione
  • Collaborazione tra team
  • Gasdotti CI/CD

Esempio di Real-World: Testing My Blog API

Ecco uno script che uso per testare l'endpoint di ricerca 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");
    }
}

Eseguilo:

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

Sommario

Gli script CSX sono il punto di riferimento perfetto tra il C# REPL e un progetto completo. Sono ideali per:

  • Velocità: Scrivi ed esegui in pochi secondi
  • Semplicità: Nessuna cerimonia di progetto
  • Potenza: Full C# con il supporto NuGet
  • Portabilità: Condividere un singolo file

La prossima volta che devi testare qualcosa di veloce in C#, salta dotnet new console e raggiungere per dotnet script Invece.

Risorse:

Finding related posts...
logo

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