C#.NETASP.NET CoreGeolocationTutorial
C# .NET IP Geolocation: ASP.NET Core, HttpClient & Minimal API Examples
ApogeoAPI6 min read
Why use a REST API for IP geolocation in .NET?
The two common .NET approaches are embedding a MaxMind GeoLite2 database file or calling a REST API. A REST API wins for most SaaS apps: no 70 MB binary to ship, no weekly update job, and no GeoIP2.DatabaseReader to manage in memory. You call an endpoint and get JSON back.
Response model (typed C#)
// GeoResponse.cs
using System.Text.Json.Serialization;
public class GeoResponse
{
[JsonPropertyName("ip")]
public string Ip { get; set; } = string.Empty;
[JsonPropertyName("country_code")]
public string CountryCode { get; set; } = "US";
[JsonPropertyName("country_name")]
public string CountryName { get; set; } = string.Empty;
[JsonPropertyName("city")]
public string? City { get; set; }
[JsonPropertyName("latitude")]
public double? Latitude { get; set; }
[JsonPropertyName("longitude")]
public double? Longitude { get; set; }
[JsonPropertyName("currency_code")]
public string? CurrencyCode { get; set; }
[JsonPropertyName("timezone")]
public string? Timezone { get; set; }
}
Service class with HttpClient and DI
// GeoService.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;
public interface IGeoService
{
Task<GeoResponse> GetAsync(string ipAddress, CancellationToken ct = default);
Task<GeoResponse> GetCallerAsync(HttpContext httpContext, CancellationToken ct = default);
}
public class GeoService : IGeoService
{
private readonly HttpClient _http;
private readonly IMemoryCache _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1);
public GeoService(HttpClient http, IMemoryCache cache)
{
_http = http;
_cache = cache;
}
public async Task<GeoResponse> GetAsync(string ip, CancellationToken ct = default)
{
// Cache by /24 subnet to reduce quota usage
var subnet = string.Join('.', ip.Split('.')[..3]);
if (_cache.TryGetValue(subnet, out GeoResponse? cached)) return cached!;
var response = await _http.GetFromJsonAsync<GeoResponse>(
$"v1/ip/{Uri.EscapeDataString(ip)}", ct)
?? new GeoResponse { Ip = ip };
_cache.Set(subnet, response, CacheTtl);
return response;
}
public async Task<GeoResponse> GetCallerAsync(HttpContext ctx, CancellationToken ct = default)
{
var ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
// Respect reverse-proxy header
if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff))
ip = xff.ToString().Split(',')[0].Trim();
return await GetAsync(ip, ct);
}
}
Registering with DI in Program.cs
// Program.cs
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<IGeoService, GeoService>(client =>
{
client.BaseAddress = new Uri("https://api.apogeoapi.com/");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", builder.Configuration["ApogeoAPI:ApiKey"]);
client.Timeout = TimeSpan.FromSeconds(3);
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5)); // avoid socket exhaustion
Using in a controller
// PricingController.cs
[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
private readonly IGeoService _geo;
public PricingController(IGeoService geo) => _geo = geo;
[HttpGet]
public async Task<IActionResult> GetPricing(CancellationToken ct)
{
var geo = await _geo.GetCallerAsync(HttpContext, ct);
var price = geo.CountryCode switch
{
"US" => new { Currency = "USD", Amount = 29, Symbol = "$" },
"GB" => new { Currency = "GBP", Amount = 25, Symbol = "£" },
"DE" or "FR" or "ES" or "IT" => new { Currency = "EUR", Amount = 27, Symbol = "€" },
"AU" => new { Currency = "AUD", Amount = 45, Symbol = "A$" },
_ => new { Currency = "USD", Amount = 29, Symbol = "$" },
};
return Ok(new { geo.CountryCode, geo.CountryName, price.Currency, price.Amount, price.Symbol });
}
}
Minimal API version (.NET 6+)
app.MapGet("/api/geo", async (IGeoService geo, HttpContext ctx, CancellationToken ct) =>
{
var result = await geo.GetCallerAsync(ctx, ct);
return Results.Ok(new
{
country = result.CountryCode,
city = result.City,
lat = result.Latitude,
lng = result.Longitude,
tz = result.Timezone,
});
})
.WithName("GetCallerGeo")
.WithOpenApi();
Middleware — block or redirect by country
// GeoBlockMiddleware.cs
public class GeoBlockMiddleware
{
private static readonly HashSet<string> BlockedCountries = new() { "CN", "RU", "KP", "IR" };
private readonly RequestDelegate _next;
private readonly IGeoService _geo;
public GeoBlockMiddleware(RequestDelegate next, IGeoService geo)
{
_next = next;
_geo = geo;
}
public async Task InvokeAsync(HttpContext ctx)
{
var geo = await _geo.GetCallerAsync(ctx);
if (BlockedCountries.Contains(geo.CountryCode))
{
ctx.Response.StatusCode = 451; // Unavailable For Legal Reasons
await ctx.Response.WriteAsync("Service not available in your region.");
return;
}
await _next(ctx);
}
}
// Register in Program.cs:
// app.UseMiddleware<GeoBlockMiddleware>();
appsettings.json
{
"ApogeoAPI": {
"ApiKey": "your-api-key-here"
}
}
In production, set ApogeoAPI__ApiKey as an environment variable (double underscore = nested key in .NET config).
Resources
- ApogeoAPI IP Geolocation docs — free tier, 1,000 req/month
- Microsoft HttpClient guidelines
Try ApogeoAPI free
1,000 requests/month forever. 14-day full-access trial. No credit card.
Get your free API key