# Käyttämällä CSX-skriptejä nopeaan C#-testaukseen

Täytyykö C#-koodia testata ilman koko projektia? C# Script -tiedostot (`.csx`) voit kirjoittaa ja pyörittää C#-koodia kuin käsikirjoituskieltä. `Program.cs`, ei `.csproj`, no build step - vain kirjoittaa ja ajaa. Täydellinen testaamaan API, validointi logiikkaa, tai prototyypitys ennen kuin sitoudut täyteen toteutukseen.

Käytän koko semanti c hakutoimintoa. Ajattelin jakaa käyttötavat. `.csx` Tapauskohtaisia testejä koskevat tiedostot tässä ja muissa projekteissa.

<datetime class="hidden">2025-11-26T20:00</datetime>

<!--category-- C#, Testing, Scripting, dotnet-script -->
[TOC]

## CSX vs. NET 10 Tiedostopohjaiset sovellukset

Ennen kuin hyppäät sisään, puhutellaan huoneessa olevaa norsua: .NET 10:ssä on nyt alkuperäiset "tiedostopohjaiset sovellukset", joiden avulla voit juosta `.cs` tiedostot suoraan `dotnet run app.cs`Millainen tämä on verrattuna CSX:ään?

### .NET 10 Tiedostopohjaiset sovellukset (nyt saatavilla!)

.NET 10 -verkolla voit ajaa yhden tiedoston C# suoraan:

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

**Ominaisuudet:**

- SDK:n alkuasukas - lisätyökaluja ei tarvita
- Käyttää tuttuja `.cs` jatkoaika
- NuGet-viitteet `#:package` Direktiivi
- Täydellinen vianetsintätuki ensimmäisestä päivästä alkaen
- Sama kääntäjä kuin tavalliset projektit

```csharp
// app.cs - .NET 10 style
#:package Newtonsoft.Json@13.0.3

using Newtonsoft.Json;

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

### CSX Scripts (Available Now)

CSX:n kautta `dotnet-script` on ollut liikkeellä vuodesta 2017:

```bash
# Available today
dotnet script app.csx
```

**Ominaisuudet:**

- Toimii .NET 6, 7, 8, 9, 10
- Rikas ekosysteemi ja työkalut
- REPL-tila vuorovaikutteista etsintää varten
- Todennettu ja taistelut testattu

```csharp
// 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));
```

### Mitä sinun tulisi käyttää?

Facte CSX (dotnet-script) NET 10 Tiedostosovellukset
|---------|---------------------|-------------------|
| **Käytettävyys** NET 6+ NET 10 NET 10
| **Asennus** | `dotnet tool install -g dotnet-script` SDK:han rakennettu
| **Tiedostotunniste** | `.csx` | `.cs` |
| **NuGet-syntaksi** | `#r "nuget: Pkg, Ver"` | `#:package Pkg@Ver` |
| **REPL-tila** Kyllä, ei vielä
| **IDE-tuki** Hyvä (VS-koodi, ratsastaja) Parannus
| **Vianetsintä** Kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä, kyllä

**Suositukseni:**

- **Kokeile ensin .NET 10 -tiedostosovelluksia** - natiivien tuki tarkoittaa vähemmän työkalujen ylimenoa
- **Vetäytykää takaisin CSX:ään** jos tarvitset REPL-tilaa tai olet vanhemmalla .NET-versiolla
- Käsitteet ovat lähes identtiset - tiedonsiirrot helposti molempien välillä

Loput artikkelista kattaa CSX:n, joka toimii edelleen hyvin ja jossa on joitakin ominaisuuksia (kuten REPL), joita .NET 10 -tiedostosovelluksilla ei vielä ole.

## Mikä on CSX?

CSX (C# Script) -tiedostot ovat C#-kooditiedostoja, jotka voidaan suorittaa suoraan ilman projektin koostamista. Ajattele sitä "Python-tyylisenä" C#:nä - kirjoitat koodin, suoritat sen, näet tulokset.

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

Suorita se:

```bash
dotnet script hello.csx
```

Siinä kaikki. `Main()` Menetelmä, ei nimitilaa, ei luokkakäärettä.

## Asennetaan dotnet-skripti

Suosituin tapa käyttää CSX-tiedostoja on [dotnet-skripti](https://github.com/dotnet-script/dotnet-script):

```bash
dotnet tool install -g dotnet-script
```

Varmista asennus:

```bash
dotnet script --version
```

## Missä CSX sopii testisykliin

Ennen kuin sukeltaa "miten", ymmärretään "milloin". CSX-käsikirjoituksissa on ainutlaatuinen paikka testauspyramidissa:

```
                    ┌─────────────────┐
                    │   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:n käsikirjoitukset "Mini-integraatiokokeina"

CSX-käsikirjoitukset eivät korvaa virallisia testejä - ne ovat **Täydennys**Ajattele heitä seuraavasti:

- **Käyttöä edeltävät piikit** - Varmista API:n työt ennen palvelun rakentamista sen ympärille
- **Vianetsintäavustimet** - Eristä ja lisää ongelmia ilman koko sovelluksen uudelleenrakentamista
- **Valmistelevat testit** - Ymmärrä, miten kirjasto käyttäytyy ennen yksikkötestien kirjoittamista
- **Savutestit** - Nopeat mielenterveyden tarkastukset todellisiin palveluihin (tietokanta, rajapinnat, jonot)

### Kehitystyön virta

Näin CSX sopii tyypilliseen kehityssykliin:

```
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
```

### Todellinen esimerkki: Umamin integraation rakentaminen

Kun rakensin tätä blogia varten Umamin analytiikkaintegraation, työnkulkuni oli:

1. **CSX: Testaa raaka API** Miltä vastaukset näyttävät?
2. **CSX: Testiaikaleiman muuntaminen** - Löysin vian täältä ennen tuotantokoodin kirjoittamista!
3. **Toteuta: Rakenna UmamiClient** - Luottaen, koska olin jo vahvistanut API:n
4. **xUnit: Write unit -testit** - Mock HttpClient, testisarjalogiikka
5. **CSX: Debug-tuotantokysymys** Metrics palaa tyhjänä?

CSX:n käsikirjoitukset eivät korvanneet yksikkökokeitani - ne **esti minua kirjoittamasta koodia, joka ei toimisi** ja auttoi minua **debug-asiat nopeammin** kun ne sattuivat.

## Miksi käyttää CSX:ää testaukseen?

### 1. Nollaseremonia

Perinteinen lähestymistapa API-puhelun testaamiseen:

1. Luo uusi konsoliprojekti
2. Lisää NuGet-paketteja
3. Kirjoita `Program.cs`
4. Rakentaa
5. Juokse
6. Poista projekti, kun se on tehty

CSX-lähestymistapa:

1. Kirjoita skripti
2. Suorita skripti

### 2. Inline NuGet Referenssit

Tarvitsetko paketin? Viittaa suoraan käsikirjoitukseesi:

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

Ensimmäinen suoritus lataa paketteja. Seuraavat suoritukset käyttävät välimuistia.

### 3. Reference Local DLLs

Testaatko omaa kirjastoasi? Viittaa suoraan:

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

using MyLibrary;

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

### 4. Reference muut käsikirjoitukset

Jaa monimutkaiset skriptit uudelleenkäytettäviin osiin:

```csharp
#load "helpers.csx"
#load "config.csx"

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

## Todellisia esimerkkejä tästä projektista

Nämä eivät ole keksittyjä esimerkkejä, vaan oikeita käsikirjoituksia, joita käytän tämän blogin koodiston vianetsintään ja testaamiseen. Jokainen niistä ratkaisi todellisen ongelman, jonka kohtasin kehityksen aikana.

### API-aikaleiman testaus

**Ongelma:** Umamin analytiikkaintegraationi palautti tyhjiä tietoja. Tuntien vianjäljityksen jälkeen epäilin aikaleiman muuntamista vääräksi - Umamin API odottaa Unix-aikaleimaa millisekunneissa, mutta en ollut varma, tuottaako .net-koodini oikeaa muotoa.

**Miksi CSX?** Olisin voinut lisätä kirjauksen tuotantokoodiin, rakentaa sen uudelleen, ottaa sen käyttöön ja tarkistaa lokit tai kirjoittaa nopean käsikirjoituksen, jolla voin todentaa hypoteesini 30 sekunnissa.

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

**Tulos:** Tämä kirjoitus todisti, että aikaleima oli tulevaisuudessa yksi vuosi. Jäljitin vian takaisin käyttämällä `DateTime.Now` Sen sijaan, että `DateTime.UtcNow` Tuotantokoodissa. Kiinteä 5 minuutissa, ei mahdollisesti 5 tunnin vianetsintää.

### Testataan Query String -sukupolvea

**Ongelma:** Minun piti varmistaa, että ASP.NET `QueryHelpers` Luokalla luodaan kyselyjonoja Umami API:n odottamassa muodossa. Koodaako se URL-koodin erikoismerkkejä? Missä järjestyksessä muuttujat ovat?

**Miksi CSX?** Dokumentaation lukeminen on eri asia, mutta todellisen ulostulon näkeminen kertoo tarkalleen, mitä koodisi tuottaa.

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

### Testataan Raw HTTP API -puheluita

**Ongelma:** Ennen täyden huoltoluokan rakentamista riippuvuusruiskeella, virheiden käsittelyllä, logiikalla ja yksikkökokeilla halusin varmistaa, että API todella toimii ja ymmärtää sen vastausformaatin.

**Miksi CSX?** On nopeampaa kirjoittaa 50 riviä tunnustelukoodia kuin rakentaa kunnollista palveluinfrastruktuuria. Jos API ei toimi niin kuin odotan, olen tuhlannut viisi minuuttia viiden tunnin sijaan.

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

### Testit riippuvuusruiskeella

**Ongelma:** Olen julkaissut NuGet-paketin (Umami.Net) ja haluan testata sitä täsmälleen niin kuin kuluttaja käyttäisi sitä - kunnollisella riippuvuusruiskeella, en pikaistamalla kursseja suoraan.

**Miksi CSX?** Luomalla testikonsoliprojektin, lisäämällä NuGet-viittaukseni, kirjoittamalla kaikki DI-kattilalevy - se on 15 minuuttia seremoniaa. CSX:n avulla voin varmistaa kuluttajakokemuksen alle kahdessa minuutissa.

```csharp
#!/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 -tietokannan testaus

**Ongelma:** Integroin Qdrant-vektoritietokantaa semanttista hakua varten. Ennen tuotantopalvelun kirjoittamista minun on ymmärrettävä, miten gRPC-asiakas toimii, miltä API näyttää, ja varmistettava, että paikallinen Qdrant-instanssini toimii oikein.

**Miksi CSX?** Vektoritietokannat ovat uusi alue monille kehittäjille. CSX antaa minun kokeilla vuorovaikutteisesti, kokeilla erilaisia toimintoja ja nähdä välittömiä tuloksia ennen kuin sitoudun arkkitehtuuriin.

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

## Käytännöllisiä esimerkkejä

### HTTP:n päätepisteen testaaminen

```csharp
#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-sarjan testaus

```csharp
#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, "scott@example.com");

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

### Testaustietokannan kyselyt

```csharp
#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-mallien testaus

```csharp
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",
    "test@example.com",
    "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}");
    }
}
```

### LINQ-kyselyiden testaus

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

### Testataan Qdrantin vektorihakua

```csharp
#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-tuki

### Kuvastudion koodi

Asenna [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) Jatkoaika. Saat:

- Syntaksin korostus
- IntelliSense
- Suorita/debugoi CodeLensin kautta

Luo `.vscode/launch.json`:

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

### JetBrains Rider

Riderillä on sisäänrakennettu CSX-tuki. `.csx` tiedosto ja valitse "Juokse".

## Vinkkejä ja temppuja

### Käytä Shebagia

Lisää shebang tehdäksesi skriptejä suoritettaviksi suoraan Linux/Macilla:

```csharp
#!/usr/bin/env dotnet-script

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

### Perustelut olettamuksilla

Käytä komentorivin argumentteja maailmanlaajuisesti `Args` muuttuja:

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

### Salaisuuksien ympäristömuuttujat

Älä koskaan käytä kovakoodisia salaisuuksia – käytä ympäristömuuttujia:

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

### Vuorovaikutteinen tila (REPL)

Aloita interaktiivinen tutkimussessio:

```bash
dotnet script
```

Saat C# REPL:n:

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

### Vianetsintä

Vianetsintä VS-koodilla lisäämällä keskeytyspiste ja ajamalla F5:llä, tai:

```bash
dotnet script test.csx --debug
```

### Käytä pikailmoitusten tallenteita

Luokan tiedostoja ei tarvita - määrittele inline:

```csharp
// 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, "scott@example.com");
var response = new ApiResponse<Person>(true, person, null);
```

### Nätti tuloste hylsyllä

```csharp
#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
```

## Yleiset kysymykset & Gotchat

### Numero: "NuGet-pakettia ei löytynyt"

Ensimmäinen kierros on hidas - paketteja ladataan taustalla:

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

**Korjaa**: Odotetaan, että ensimmäinen ajo valmistuu, tai ennen lataamista:

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

### Aihe: "Tyyppiä tai nimitilaa ei löytynyt"

Pakettiversio voi olla väärä tai yhteensopimaton:

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

### Julkaisu: gRPC Windowsissa

Qdrant- ja muut gRPC-palvelut epäonnistuvat HTTP/2-virheissä:

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

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

### Julkaisu: HttpClient Socket Remotion

Älä luo useita HttpClient-tapauksia silmukkaan:

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

### Aihe: Async huipputasolla

Huipputason async toimii vain CSX:ssä - async Mainia ei tarvita:

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

### Kysymys: Konfliktit kasassa

Viitattaessa paikallisiin, joilla on riippuvuuksia:

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

### Julkaisu: Script ei kulje muokkauksen jälkeen

IntelliSense-välimuisti voi olla tunkkainen:

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

### Myönnetään: Käytössä olevat viitetyypit

CSX käyttää erilaisia oletuksia – ota käyttöön tarvittaessa erikseen:

```csharp
#nullable enable

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

## Milloin käyttää CSX:ää ja koko projektia

**Käytä CSX:ää, kun:**

- Nopeat kertatestit
- API-etsintä
- Prototyyppialgoritmeja
- Testataan NuGet-paketteja ennen projektiin lisäämistä
- Validointi regex, LINQ, JSON serialisointi
- Tietokannan kyselyn testaus
- Oppiminen/kokeilu

**Käytä täyttä projektia, kun:**

- Useita tiedostoja, joissa on monimutkaisia riippuvuuksia
- Yksikkötestaus (käytä xUnitia/NUnitia)
- Tuotantokoodi
- Tiimiyhteistyö
- CI/CD-putket

## Real-World Example: Testi Oma blogi API

Tässä on käsikirjoitus, jolla testaan Enimmäkseen lusid-haun päätetapahtumaa:

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

Suorita se:

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

## Yhteenveto

CSX-skriptit ovat täydellinen keskikenttä C# REPL:n ja koko projektin välillä. Ne ovat ihanteellisia:

- **Nopeus**: Kirjoita ja juokse sekunneissa
- **Yksinkertaisuus**: Ei projektitilaisuutta
- **Voima**: Täysi C# NuGet-tuella
- **Siirrettävyys**: Jaa yksi tiedosto

Kun seuraavan kerran testaat jotain nopeasti C#:ssa, jätä väliin `dotnet new console` ja tavoita `dotnet script` Sen sijaan.

**Resurssit:**

- [dotnet-script GitHub](https://github.com/dotnet-script/dotnet-script)
- [C# Käsikirjoitusdokumentaatio](https://docs.microsoft.com/en-us/archive/msdn-magazine/2016/january/essential-net-csharp-scripting)