Step-by-step: Integrating Google Gemini into ASP.NET Core project
We have recently seen a trend of APIs powered by Large Language Models (LLM), I decided to do some research and give myself a try with building such an API, and guess what, it’s awasome!
For the past two weeks, I’ve been delving into Google’s latest LLM, Gemini. After gaining some knowledge, I tested it in real use case, and really fell in love. However, this guide won’t focus on a specific API use case. Instead, we’ll explore the step-by-step process of configuring the Google Gemini API within your ASP.NET Core project. So, let’s dive in.
Before code
Before we dive in, it’s crucial to get acquainted with the Google Gemini API documentation. Understanding the fundamentals of any external API is essential for successful integration into your project.
Once you’re familiar with the documentation, it’s time to obtain your Google Gemini API key here. However, be aware that currently, Gemini API access may be limited in certain regions, including Europe.
Setup your project
Now that you have your API key, we can configure it within your ASP.NET Core project’s appsettings.Development.json
file. Here's an example:
"Gemini": {
"ApiKey": "***************************************",
"Url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
}
Accordingly I recommend to create class which will hold those values:
internal sealed class GeminiOptions
{
public string ApiKey { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}
Why Headers over URL Parameters?
While both methods work for passing the API key, sending it in the request header is generally considered a more secure practice. This is because:
- Security: Headers are not logged as part of the URL, reducing the risk of exposing your key in logs or browser history.
- Separation of Concerns: Keeping the API key separate from the URL maintains a cleaner code structure.
Building Request and Response Models
While anonymous types can be used for API requests, adhering to object-oriented principles offers several benefits. Let’s define a custom types to represent the request body for clarity and maintainability
Request models
// Our request body
internal sealed class GeminiRequest
{
public GeminiContent[] Contents { get; set; }
public GenerationConfig GenerationConfig { get; set; }
public SafetySetting[] SafetySettings { get; set; }
}
internal sealed class GeminiContent
{
public string Role { get; set; }
public GeminiPart[] Parts { get; set; }
}
internal sealed class GeminiPart
{
// This one interests us the most
public string Text { get; set; }
}
// Two models used for configuration
internal sealed class GenerationConfig
{
public int Temperature { get; set; }
public int TopK { get; set; }
public int TopP { get; set; }
public int MaxOutputTokens { get; set; }
public List<object> StopSequences { get; set; }
}
internal sealed class SafetySettings
{
public string Category { get; set; }
public string Threshold { get; set; }
}
While the code might seem extensive at first glance, the core element we’ll be using is the GeminiPart.Text
property. This property holds the actual text you want Gemini to process. In this initial step, we’ll focus on keeping things simple by utilizing the default configuration values. You can explore customization options later as needed.
Response models
// Response body
internal sealed class GeminiResponse
{
public Candidate[] Candidates { get; set; }
public PromptFeedback PromptFeedback { get; set; }
}
internal sealed class PromptFeedback
{
public SafetyRating[] SafetyRatings { get; set; }
}
internal sealed class Candidate
{
public Content Content { get; set; }
public string FinishReason { get; set; }
public int Index { get; set; }
public SafetyRating[] SafetyRatings { get; set; }
}
internal sealed class Content
{
public Part[] Parts { get; set; }
public string Role { get; set; }
}
internal sealed class Part
{
// This one interests us the most
public string Text { get; set; }
}
internal sealed class SafetyRating
{
public string Category { get; set; }
public string Probability { get; set; }
}
As you can see, the response also includes a lot of data. While not useless information, it’s not necessary for basic API usage. Primarly we’ll focus on Part.Text
this property holds the generated text response from the API. This is the primary information you’ll be interested in for basic usage.
Streamlining Request Creation with a Factory Pattern
To ensure code readability, maintainability, and reusability, we’ll leverage the power of the factory pattern to construct our Gemini API request bodies. This pattern gracefully handles object creation with default configuration values, keeping our code streamlined and organized.
internal sealed class GeminiRequestFactory
{
public static GeminiRequest CreateRequest(string prompt)
{
return new GeminiRequest
{
Contents = new GeminiContent[]
{
new GeminiContent
{
Role = "user",
Parts = new GeminiPart[]
{
new GeminiPart
{
Text = prompt
}
}
}
},
GenerationConfig = new GenerationConfig
{
Temperature = 0,
TopK = 1,
TopP = 1,
MaxOutputTokens = 2048,
StopSequences = new List<object>()
},
SafetySettings = new SafetySettings[]
{
new SafetySettings
{
Category = "HARM_CATEGORY_HARASSMENT",
Threshold = "BLOCK_ONLY_HIGH"
},
new SafetySettings
{
Category = "HARM_CATEGORY_HATE_SPEECH",
Threshold = "BLOCK_ONLY_HIGH"
},
new SafetySettings
{
Category = "HARM_CATEGORY_SEXUALLY_EXPLICIT",
Threshold = "BLOCK_ONLY_HIGH"
},
new SafetySettings
{
Category = "HARM_CATEGORY_DANGEROUS_CONTENT",
Threshold = "BLOCK_ONLY_HIGH"
}
}
};
}
}
Ensuring authorization with a custom DelegatingHandler
For a seamless and secure connection to the Gemini API, we’ll create a GeminiDelegatingHandler
that automatically injects the necessary authorization header into every request. This safeguards against accidental omissions and keeps our code focused on core functionality.
internal sealed class GeminiDelegatingHandler(IOptions<GeminiOptions> geminiOptions)
: DelegatingHandler
{
private readonly GeminiOptions _geminiOptions = geminiOptions.Value;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("x-goog-api-key", $"{_geminiOptions.ApiKey}");
return base.SendAsync(request, cancellationToken);
}
}
The IOptions<GeminiOptions> parameter injects the API key from appsettings.Development.json
then it’s assigned to _geminiOptions field and used in SendAsync method to add authorization header to request
Building Gemini Client
Now, let’s tie everything together! The GeminiClient
class acts as the central point for interacting with the Gemini API. It takes care of request creation, sending, and processing the response to extract the generated content.
internal sealed class GeminiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeminiClient> _logger;
private readonly JsonSerializerSettings _serializerSettings = new()
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
public GeminiClient(HttpClient httpClient, ILogger<GeminiClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<string> GenerateContentAsync(string prompt, CancellationToken cancellationToken)
{
var requestBody = GeminiRequestFactory.CreateRequest(prompt);
var content = new StringContent(JsonConvert.SerializeObject(requestBody, Formatting.None, _serializerSettings), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("", content, cancellationToken);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var geminiResponse = JsonConvert.DeserializeObject<GeminiResponse>(responseBody);
var geminiResponseText = geminiResponse?.Candidates[0].Content.Parts[0].Text;
return geminiResponseText;
}
}
Explanation:
- Request creation: It leverages the
GeminiRequestFactory.CreateRequest
method to build aGeminiRequest
then it is serialized to JSON and Request Body Content is created - Response Sending and Processing: After sending the request and ensuring it’s successful, the method extracts the generated content and returns it as a string
Customization Points:
- Error Handling: The current implementation throws an exception on non-successful status codes. You can modify this behavior to handle errors gracefully.
- Return Type: The method returns a string containing the generated text. You can modify it to return the entire
GeminiResponse
object for more detailed information.
Remember:
- Snake case serialization: Google Gemini API requires snake_case naming convention, so remember to follow up this rule and set up JsonSerializerSettings properly
Configuring Dependency Injection
To orchestrate a seamless symphony of components, we’ll configure dependency injection to make Gemini Client ready for action
private static void AddGemini(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<GeminiOptions>(configuration.GetSection("Gemini"));
services.AddTransient<GeminiDelegatingHandler>();
services.AddHttpClient<GeminiClient>(
(serviceProvider, httpClient) =>
{
var geminiOptions = serviceProvider.GetRequiredService<IOptions<GeminiOptions>>().Value;
httpClient.BaseAddress = new Uri(geminiOptions.Url);
})
.AddHttpMessageHandler<GeminiDelegatingHandler>();
}
Conclusion
As you can see, it isn’t hard to create fascinating and trendy APIs that uses LLM. Moreover, once you’ve created such a Gemini Client, you can reuse it in numerous projects, that’s awasome!
In near future I’ll also write about usage of this service in business logic, so stay tuned!
Code and have fun!