.NETModel ProvidersC#Tutorial

Using Google Gemini in C# and .NET

How to call Google's Gemini models from C#: get an API key, send a prompt, stream responses, and use Gemini through a provider-agnostic interface in .NET.

Using Google Gemini in C# and .NET

Google’s Gemini models round out the big three (alongside OpenAI and Claude), and they’re worth having in your toolkit — strong multimodal support and competitive pricing. This guide shows how to call Gemini from C#, whether through its REST API directly or a provider-agnostic .NET abstraction.

Getting access

You can reach Gemini two ways:

  • Google AI Studio — a simple API key, quickest to start with.
  • Vertex AI — Gemini through Google Cloud, with IAM/Workload Identity auth. Use this for production on GCP (and it pairs with deploying on GKE via Workload Identity — no keys).

For a first call, grab a key from AI Studio and store it in an environment variable.

Treat that as an architectural decision rather than a signup detail, because it decides who can call your models and how you prove it. An API key is a single bearer credential: anyone holding it can spend your quota, from anywhere, and the only revocation granularity is the key itself. Vertex AI replaces that with the identity model you already use on GCP — service accounts, IAM roles, VPC Service Controls, audit logs in Cloud Logging, and requests pinned to a region you chose. If a compliance conversation is anywhere in your future, start on Vertex; retrofitting it later means changing the endpoint, the auth path and the quota model at the same time.

Calling the REST API from C#

Gemini’s generateContent endpoint is a straightforward HTTP POST:

using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-goog-api-key", Environment.GetEnvironmentVariable("GEMINI_API_KEY"));

const string url =
    "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent";

var body = new
{
    contents = new[]
    {
        new { role = "user", parts = new[] { new { text = "Explain eventual consistency in two sentences." } } }
    }
};

var res = await http.PostAsJsonAsync(url, body);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
var text = json.GetProperty("candidates")[0]
    .GetProperty("content").GetProperty("parts")[0].GetProperty("text").GetString();
Console.WriteLine(text);

The key goes in the x-goog-api-key header rather than a ?key= query parameter. Both work, but a secret in a query string is a secret in your access logs, your reverse proxy logs, any APM trace that records the request URL, and the Referer header of anything downstream. Header auth costs one extra line and removes a whole category of accidental disclosure.

Note Gemini’s request shape differs from OpenAI’s: content is a list of parts, not messages with roles (though there’s a role-based structure for multi-turn).

That role structure has one detail that catches everyone porting from another provider: the assistant’s turn is role: "model", not "assistant". Send "assistant" and you get a validation error rather than a working conversation. Alongside contents, the request also takes systemInstruction (a top-level instruction block, not a message in the array) and generationConfig, which is where temperature, maxOutputTokens and structured-output settings live rather than sitting at the top level.

Reading the response without getting burned

candidates[0] is the line that will page you at 3am. The candidates array is not guaranteed to be populated. When a prompt is blocked before generation starts, you get a 200 response with no candidates at all and a promptFeedback.blockReason explaining why — so the indexing above throws an IndexOutOfRangeException, and the exception tells you nothing about the actual cause.

Even when a candidate exists, finishReason decides whether you can trust it. STOP means the model finished normally. MAX_TOKENS means it was cut off at your output budget — a real, complete-looking string that simply stops halfway. SAFETY means generation was halted partway through, which can leave you with a partial answer or none. Reading parts[0].text without checking gives you a truncated or empty answer indistinguishable from a good one.

if (json.TryGetProperty("promptFeedback", out var feedback) &&
    feedback.TryGetProperty("blockReason", out var reason))
{
    // The prompt never reached the model. Surface this — do not retry blindly.
    throw new InvalidOperationException($"Prompt blocked: {reason.GetString()}");
}

if (!json.TryGetProperty("candidates", out var candidates) || candidates.GetArrayLength() == 0)
    throw new InvalidOperationException("No candidates returned.");

var finishReason = candidates[0].GetProperty("finishReason").GetString();
// "STOP" is the only value that means "this is a complete answer".

The same response carries usageMetadata with promptTokenCount, candidatesTokenCount and totalTokenCount. Log those from the first day. Retrofitting token accounting after a surprising invoice means guessing at attribution; capturing it from the start means you can answer “which feature costs what” in a query.

Streaming

For anything a user watches, use streamGenerateContent with alt=sse instead — same request body, same model, but responses arrive as server-sent events carrying partial candidates rather than one blob at the end.

const string streamUrl =
    "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse";

The alt=sse part matters. Without it the endpoint returns a JSON array of chunks that most HTTP clients will happily buffer to completion, so you get all the complexity of streaming and none of the benefit. With it you get a standard text/event-stream you can read incrementally.

Beyond perceived latency, streaming is what keeps long generations alive. A non-streaming request with a large maxOutputTokens holds a connection open with no traffic on it until the model finishes, and every idle timeout in the path — HttpClient’s default 100 seconds, a load balancer, a reverse proxy — is entitled to kill it. The symptom is a TaskCanceledException that looks like a network fault and isn’t.

The portable way: Microsoft.Extensions.AI

As with every provider, the maintainable approach is to hide Gemini behind Microsoft.Extensions.AI’s IChatClient, so your app and any agent code is model-agnostic:

IChatClient chat = /* a Gemini-backed IChatClient */;
var reply = await chat.GetResponseAsync("Give me three uses for a message queue.");

Then Gemini, GPT, and Claude are interchangeable, and you can benchmark them on your own eval set.

The portability is real for the common path — messages in, text or tool calls out, streaming, structured output — and stops at the provider-specific surface. Gemini’s safety settings are the obvious example: they have no counterpart in the shared interface, so if your application depends on tuning them, that dependency reaches past the abstraction. So does anything relying on promptFeedback, since “the prompt was blocked before generation” isn’t a concept every provider has.

None of that is a reason to skip the abstraction. It’s a reason to keep the leaks in one place — a thin wrapper of your own around the two or three Gemini-specific behaviours you actually rely on — so switching provider is a rewrite of one class rather than an archaeology exercise across the solution.

Where Gemini shines

Gemini’s strong multimodal support (text, images, and more in one request) makes it a natural pick when your input isn’t just text — analyzing screenshots, documents, or diagrams. If your workload is multimodal, it’s worth benchmarking Gemini specifically rather than defaulting to a text-first model.

Mechanically there are two ways to attach media. Small files go inline as an inlineData part carrying a mimeType and base64 content, which is easy and counts against the total request size — so past a few megabytes you’ll hit request limits and need the other route. Larger or reused files go through the Files API: upload once, get back a URI, and reference it as a fileData part on as many requests as you like. For a workload that asks ten questions about the same PDF, that’s the difference between uploading it ten times and uploading it once.

Budget for the token cost. Images and especially video consume input tokens at a rate that surprises people used to text — a short video clip can dwarf a long document. This is where the response’s usageMetadata stops being a nice-to-have: without it, a multimodal feature’s cost profile is invisible until the invoice arrives.

One question splits the two cases: content that is small and known goes in the prompt, content you have to search for gets searched, with the three costs that compound at scale.A large window makes the first category much bigger. It does not abolish the second.Where is the content?Small, and you know where it isput it in the promptYou have to search for itso search for itWHY STUFFING STOPS BEING RIGHT AT SCALE — THREE COSTS THAT COMPOUND1you pay for every input token on every request, so a 200-page prompt asked five questions costs five times2latency scales with input length, so time to first token gets noticeably worse3accuracy does not improve monotonically — the right paragraph buried among a hundred wrong ones is harder,and the failure is a confident answer drawn from the wrong section
For a single document a user just uploaded, stuffing the full text in really is simpler, more accurate and cheaper to build than a pipeline you have to maintain. The trap is generalising from that case to a corpus, where all three costs below arrive together.

Long context is not a substitute for retrieval

Gemini’s context windows are large enough that the tempting move is to skip retrieval entirely and paste in the whole corpus. Sometimes that’s right — for a single document a user just uploaded, stuffing the full text into the prompt is simpler, more accurate and cheaper to build than a RAG pipeline you have to maintain.

It stops being right at scale, for three reasons that compound. You pay for every input token on every request, so a 200-page prompt asked five questions costs five times a 200-page prompt. Latency scales with input length, so time-to-first-token gets noticeably worse. And accuracy doesn’t improve monotonically with context — burying the relevant paragraph among a hundred irrelevant ones makes the model’s job harder, not easier, and the failure mode is a confident answer drawn from the wrong section.

The useful rule: if the relevant content is small and known, put it in the prompt. If you have to search for it, search for it. Large context windows make that first category much bigger, which is genuinely valuable — they don’t abolish the second.

Production on Google Cloud

If you’re already on GCP, prefer Vertex AI over an API key: your app authenticates with a managed identity via Workload Identity, so there’s no key to leak. This is the same keyless pattern covered in the GKE deployment guide and recommended in securing AI agents.

Three things change when you move, and it’s better to know them before the migration than during it. The host and path change — Vertex requests go to a regional aiplatform.googleapis.com endpoint scoped to your project and location rather than to generativelanguage.googleapis.com. Auth changes from a static header to a short-lived OAuth token that Application Default Credentials fetches and refreshes for you, which is strictly better in production and slightly more setup locally. And quota is managed per project and region through Google Cloud’s quota system rather than being attached to a key, which means your capacity planning conversation is with a quota page and not a support ticket.

The request and response bodies are close enough that the model-facing code survives the move largely intact. Plan the migration around auth, endpoints and quota, not around prompts.

When not to reach for Gemini

Being specific about the boundaries is more useful than a recommendation:

Text-only workloads where you already have a working integration. Multimodality is the headline reason to choose Gemini. If your input is plain text and another provider is already wired up and evaluated, swapping for its own sake is churn.

When you need one provider’s exclusive capability. Every vendor has something the others don’t. Pick on the capability your product depends on, not on general reputation.

When you can’t yet measure the difference. “Which model is better” is unanswerable in the abstract and trivially answerable against an eval set of your own tasks. Build the eval first; the model choice then answers itself and stays answered as new versions ship.

Note: Gemini model IDs (e.g. gemini-2.5-flash) and endpoints evolve. Verify against the Gemini API docs for current models; the approach — REST generateContent or IChatClient for portability, Vertex AI for keyless production — is stable.

Takeaway

Calling Gemini from C# is a simple REST POST to generateContent, or — better — a call through Microsoft.Extensions.AI so it’s one swappable provider. Put the key in the x-goog-api-key header rather than the URL, never index candidates[0] without checking promptFeedback and finishReason, stream anything long so idle timeouts can’t cut you off, and log usageMetadata from day one so multimodal costs stay visible. Use an AI Studio key to prototype, move to Vertex AI with Workload Identity for keyless production on GCP, and lean on Gemini’s multimodal strength when your inputs go beyond plain text. Build on IChatClient and switching between Gemini, GPT, and Claude stays a config change.

Frequently asked questions

Which Gemini endpoint should a .NET app use?

Either the Gemini Developer API for the fastest start, or Vertex AI when you need Google Cloud IAM, VPC controls and regional guarantees. The model is the same; the auth story and the compliance posture are not.

Does Gemini support function calling from C#?

Yes. The declaration format differs from OpenAI's but the pattern is identical — declare the function, let the model request it, execute it, return the result. Going through Microsoft.Extensions.AI keeps that difference out of your application code.

What is genuinely different about Gemini?

Two things: very large context windows, and native multimodality — images, audio and video are first-class inputs rather than a bolt-on. If your problem involves documents or media rather than plain text, that is the reason to look at it.

How should I handle API keys in .NET?

The same way as any other secret: user-secrets in development, a managed identity or key vault in production, and never a literal in configuration that reaches source control.