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.
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 — RESTgenerateContentorIChatClientfor 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.
