YandexGPT Integration in .NET: A Practical Guide for Developers
YandexGPT is not just a cloud LLM service; it’s a fully managed API that supports asynchronous text and image generation and integrates seamlessly with enterprise .NET infrastructure. This article provides a production-ready integration implementation, addressing real-world challenges such as timeouts, operation statuses, Base64 handling, and Markdown output sanitization. We focus on what matters most in production environments: fault tolerance, type safety, prompt engineering control, and secure credential storage.
Client Architecture and Secure Initialization
The MyGPTClient class must be thread-safe and avoid resource leaks. Using a global HttpClient is an anti-pattern. Instead, we leverage IHttpClientFactory, registered via Dependency Injection:
// Program.cs
builder.Services.AddHttpClient<MyGPTClient>((sp, client) =>
{
var config = sp.GetRequiredService<IConfiguration>();
client.BaseAddress = new Uri("https://llm.api.cloud.yandex.net/");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Api-Key", config["Yandex:ApiKey"]);
});
The MyGPTClient constructor now accepts IHttpClientFactory instead of creating HttpClient directly. Model URIs (gpt://.../yandexgpt, art://.../yandex-art/latest) are moved to configuration, allowing easy switching between environments (dev/staging/prod) without recompilation.
Important: The ai.languageModels.user role requires explicit assignment in the Yandex Cloud catalog IAM policy. For CI/CD environments, a separate service account with minimal permissions—only ai.*.user—is required. No editor or admin roles.
Asynchronous Task Handling: From Polling to Retry Logic
The original WaitForTaskCompletionAsync method uses an infinite loop with a fixed Task.Delay(1000). This is unacceptable in production: with 50+ concurrent requests, it strains API limits and exhausts threads. We implement adaptive polling with exponential backoff and retry limits:
private async Task<dynamic> WaitForTaskCompletionAsync(string taskId, int maxAttempts = 30)
{
int attempt = 0;
TimeSpan delay = TimeSpan.FromMilliseconds(500);
while (attempt < maxAttempts)
{
try
{
var response = await _client.GetAsync($"operations/{taskId}");
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<dynamic>(responseBody);
if (result?.done == true && result?.response != null)
return result.response;
if (result?.error != null)
throw new InvalidOperationException($"API error: {result.error.message}");
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
// Handle 429 — increase delay
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.5, 10_000));
}
catch (JsonException)
{
throw new InvalidOperationException("Invalid JSON in task status response");
}
await Task.Delay(delay);
attempt++;
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.2, 5_000));
}
throw new TimeoutException($"Task {taskId} did not complete within {maxAttempts} attempts");
}
This solution addresses two key issues: first, it prevents API flooding during temporary unavailability; second, it throws a clear exception upon timeout—essential for proper logging and alerting.
Output Sanitization: From Markdown to Semantic HTML
YandexGPT returns Markdown with inconsistent formatting: #, ##, *, , as well as random characters (////, ----, ****). Simple regex replacements aren’t enough. We implement a parser based on Markdig, a library that supports safe conversion with whitelist mode:
public string ConvertToHtml(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown)) return string.Empty;
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UseEmojiAndSmiley()
.Build();
// Remove first-level headings—they duplicate the article’s topic
var cleaned = Regex.Replace(markdown, @"^#{1,6}\s+.*$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);
// Disable dangerous elements
var html = Markdown.ToHtml(cleaned, pipeline);
return SanitizeHtml(html); // implemented via HtmlSanitizer
}
private string SanitizeHtml(string html)
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Clear();
sanitizer.AllowedTags.UnionWith(new[] { "p", "br", "strong", "em", "ul", "ol", "li", "h2", "h3" });
sanitizer.AllowedAttributes.Clear();
return sanitizer.Sanitize(html);
}
This ensures the output is valid, safe HTML without <script>, <iframe>, or inline styles.
Image Generation: Base64 Validation and Storage
The GenerateImageAsync method requires strict MIME-type and size validation. Base64 strings can be corrupted or contain non-JPEG data. We add validation:
private bool IsValidBase64Image(string base64String)
{
if (!base64String.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) ||
!base64String.Contains(";base64,"))
return false;
var data = base64String.Substring(base64String.IndexOf(',') + 1);
if (!IsValidBase64String(data)) return false;
try
{
var bytes = Convert.FromBase64String(data);
using var ms = new MemoryStream(bytes);
using var img = Image.Load(ms);
return img.Metadata.DecodedImageOrientation == ExifOrientation.Undefined ||
img.Width > 100 && img.Height > 100;
}
catch
{
return false;
}
}
Saving is done in IWebHostEnvironment.WebRootPath/Resources/ArticleImg—the standard path for static files in ASP.NET Core. File names are generated using Path.GetRandomFileName() with a .jpg extension, rather than Guid.NewGuid(), to avoid collisions under high load.
Key Takeaways
- Security: API keys are passed via
IConfiguration, not hardcoded. Service accounts have onlyai.*.userroles. - Reliability: Polling is implemented with exponential backoff and retry limits. All HTTP errors are handled explicitly.
- Compatibility: The client uses
IHttpClientFactory, enabling Polly for retries and circuit breakers. - Output Cleanliness: Markdown-to-HTML conversion goes through
MarkdigandHtmlSanitizer, not regex replacements. - Performance: Images are saved in
WebRootPath, accessible via URL without middleware.
For testing, it’s recommended to use a mock server (e.g., WireMock.NET) emulating /foundationModels/v1/completionAsync and /operations/{id}. This covers all states: done: true, done: false, error, 429 Too Many Requests.
The full code, including DI registration, unit tests, and a controller usage example, is available in the dotnet-yandexgpt-sample repository. In production, be sure to configure Application Insights for tracing calls to the Yandex API—it’s critical for diagnosing delays and errors.
— Editorial Team
No comments yet.