Step-by-step: Integrating Google Gemini into ASP.NET Core project

DBrdak

--

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:

  1. Request creation: It leverages the GeminiRequestFactory.CreateRequest method to build a GeminiRequest then it is serialized to JSON and Request Body Content is created
  2. 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!

--

--

Responses (1)