Защо C#
Проектите ви от модул 4 са на C# и работят с база от данни. AI функцията не изисква Python — тя е обикновено HTTP извикване, което се прави еднакво добре от всеки език.
Разделение на труда
Python остава удобен за проучване, обучение на модели и измервания. За приложението, което ползва готова услуга, езикът е този на самото приложение.
Ключът в C# приложение
| Къде | Подходящо ли е | Защо |
|---|---|---|
| В кода | не | влиза в хранилището и в компилирания файл |
| В App.config до exe-то | не | потребителят го отваря с текстов редактор |
| Променлива на средата на сървъра | да | не пътува с приложението |
| Собствен сървър между клиента и модела | най-добре | настолното приложение изобщо не вижда ключа |
Настолно приложение = ключът е у потребителя
Всичко, което инсталирате на чужда машина, може да бъде прочетено. Ако приложението е настолно, ключът стои на ваш сървър, а приложението вика него — точно както в урок 15.
Извикването
Един HttpClient, зададен веднъж, а не нов за всяка заявка. Класическа грешка в C# е създаването на нов клиент при всяко извикване — води до изчерпване на портовете.
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class ModelKlient
{
private static readonly HttpClient http = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30)
};
private readonly string klyuch;
public ModelKlient(string klyuch) => this.klyuch = klyuch;
public async Task<string> PitayAsync(string sistemno, string vapros)
{
var zaqvka = new
{
system_instruction = new { parts = new[] { new { text = sistemno } } },
contents = new[]
{
new { role = "user", parts = new[] { new { text = vapros } } }
},
generationConfig = new { temperature = 0.2, maxOutputTokens = 500 }
};
using var otg = await http.PostAsJsonAsync(
$"{Adres}?key={klyuch}", zaqvka);
otg.EnsureSuccessStatusCode();
using var dok = JsonDocument.Parse(await otg.Content.ReadAsStringAsync());
return dok.RootElement
.GetProperty("candidates")[0]
.GetProperty("content")
.GetProperty("parts")[0]
.GetProperty("text")
.GetString() ?? "";
}
}static readonly HttpClient— един за целия живот на приложението.async / await— иначе интерфейсът замръзва, докато моделът отговаря.EnsureSuccessStatusCodeхвърля при 4xx и 5xx — оттам се закача обработката от урок 18.
Асинхронност в интерфейса
Извикването отнема секунди. В настолно приложение това означава замразен прозорец, ако се извика синхронно.
private async void btnPitay_Click(object sender, EventArgs e)
{
btnPitay.Enabled = false;
lblStatus.Text = "Изчаквам отговор…";
try
{
string otgovor = await klient.PitayAsync(Sistemno, txtVapros.Text);
txtOtgovor.Text = otgovor;
await ZapishiRazgovorAsync(txtVapros.Text, otgovor);
}
catch (HttpRequestException ex)
{
lblStatus.Text = "Услугата не отговори. Опитайте отново.";
Zhurnal.Zapishi(ex);
}
finally
{
btnPitay.Enabled = true;
}
}- Бутонът се изключва, за да няма двойни заявки.
- Показва се състояние — потребителят трябва да вижда, че нещо се случва.
- Грешката се показва разбираемо, а подробностите отиват в журнала.
finallyвръща интерфейса в изправност независимо от изхода.
Връзка с базата
Записът на разговора се прави със същия ADO.NET, който познавате от модул 4 — параметризирани команди, без слепване на низове.
public async Task ZapishiRazgovorAsync(string vapros, string otgovor)
{
const string sql = @"
INSERT INTO sabshtenia (razgovor_id, rolya, tekst, model, versia_prompt)
VALUES (@r, 'user', @v, NULL, NULL), (@r, 'model', @o, @m, @p);";
using var vrazka = new SqlConnection(NizNaVrazka);
await vrazka.OpenAsync();
using var kom = new SqlCommand(sql, vrazka);
kom.Parameters.AddWithValue("@r", razgovorId);
kom.Parameters.AddWithValue("@v", vapros);
kom.Parameters.AddWithValue("@o", otgovor);
kom.Parameters.AddWithValue("@m", "gemini-2.5-flash");
kom.Parameters.AddWithValue("@p", VersiaPrompt);
await kom.ExecuteNonQueryAsync();
}Същото правило като за SQL инжекция
Текстът на въпроса идва от потребителя, а отговорът — от модела. И двете са външен вход. Параметризираните команди са задължителни, не препоръка.
Структуриран изход в C#
Когато отговорът трябва да е JSON по схема (урок 25 от 11. клас), в C# той се разчита в клас — с проверка, а не с надежда.
public sealed record Izvlecheno(
[property: JsonPropertyName("grad")] string Grad,
[property: JsonPropertyName("plosht")] double Plosht,
[property: JsonPropertyName("cena")] int Cena);
Izvlecheno? rezultat = JsonSerializer.Deserialize<Izvlecheno>(otgovor);
if (rezultat is null || rezultat.Cena <= 0)
{
Zhurnal.Zapishi("Невалиден отговор от модела: " + otgovor);
return null;
}