Back to Home

YandexGPT Integration in .NET — Guide

Guide to integrating YandexGPT into .NET projects. Covers secure client initialization, asynchronous task processing with exponential backoff, validation and saving of images, Markdown conversion to safe HTML. Goal — production-ready solution for developers.

How to properly connect YandexGPT to a .NET project
Advertisement 728x90

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.

Google AdInline article slot

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.

Google AdInline article slot

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:

Google AdInline article slot
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 only ai.*.user roles.
  • 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 Markdig and HtmlSanitizer, 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

Advertisement 728x90

Read Next