C# / .NET SDK
Official .NET client for the BoxesNLines Geolocation API.
Installation
dotnet add package BoxesNLines.Geolocation
Quick Start
using BoxesNLines.Geolocation;
var geo = new GeolocationClient(new GeolocationClientOptions { ApiKey = "bnl_your_api_key" });
var result = await geo.GetCountryFromGpsAsync(34.0522, -118.2437);
Console.WriteLine(result.Data); // "USA"
Configuration
var geo = new GeolocationClient(new GeolocationClientOptions
{
ApiKey = "bnl_your_api_key",
BaseUrl = "https://api.boxesnlines.com",
TimeoutSeconds = 30,
MaxRetries = 3,
Cache = new CacheOptions
{
Enabled = true,
DefaultTtl = TimeSpan.FromMinutes(5),
MaxEntries = 1000,
},
});
Configuration Options
| Option | Default | Description |
|---|---|---|
ApiKey | (required) | Your API key starting with bnl_ |
BaseUrl | https://api.boxesnlines.com | API base URL |
TimeoutSeconds | 30 | Request timeout in seconds |
MaxRetries | 3 | Max retry attempts on 429/5xx |
Cache.Enabled | true | Enable in-memory LRU cache |
Cache.DefaultTtl | 5 minutes | Cache entry time-to-live |
Cache.MaxEntries | 1000 | Maximum cache entries |
NoCacheWarningInterval | 100 | Emit a diagnostic trace warning every N requests when caching is disabled. Set to 0 to suppress. |
Dependency Injection
using BoxesNLines.Geolocation.DependencyInjection;
// In Program.cs or Startup.cs
builder.Services.AddBoxesNLinesGeolocation(options =>
{
options.ApiKey = builder.Configuration["BoxesNLines:ApiKey"]!;
});
// In your service
public class MyService
{
private readonly IGeolocationClient _geo;
public MyService(IGeolocationClient geo)
{
_geo = geo;
}
public async Task<string> GetCountry(double lat, double lon)
{
var result = await _geo.GetCountryFromGpsAsync(lat, lon);
return result.Data;
}
}
Available Methods
GetCountryFromGpsAsync
Returns the country code for the given coordinates.
var result = await geo.GetCountryFromGpsAsync(40.7128, -74.0060);
Console.WriteLine(result.Data); // "USA"
GetSubdivisionFromGpsAsync
Returns the ISO 3166-2 subdivision code for the given coordinates.
var result = await geo.GetSubdivisionFromGpsAsync(34.0522, -118.2437);
Console.WriteLine(result.Data); // "US-CA"
IsInCountryAsync
Checks whether the given coordinates are within a country (by alpha-3 code).
var result = await geo.IsInCountryAsync("USA", 40.7128, -74.0060);
Console.WriteLine(result.Data); // true
IsInSubdivisionAsync
Checks whether the given coordinates are within a subdivision (by ISO 3166-2 code).
var result = await geo.IsInSubdivisionAsync("US-CA", 34.0522, -118.2437);
Console.WriteLine(result.Data); // true
ClearCacheAsync
Clears all cached geo responses.
await geo.ClearCacheAsync();
Error Handling
All API errors throw typed exceptions that extend ApiException:
try
{
var result = await geo.GetCountryFromGpsAsync(0, 0);
}
catch (NotFoundException ex)
{
// 404 - no country found at coordinates (e.g., ocean)
Console.WriteLine(ex.Message);
}
catch (AuthException ex)
{
// 401 - invalid or expired API key
Console.WriteLine(ex.Message);
}
catch (ValidationException ex)
{
// 400 - invalid parameters
foreach (var (field, errors) in ex.FieldErrors)
{
Console.WriteLine($"{field}: {string.Join(", ", errors)}");
}
}
catch (RateLimitException ex)
{
// 429 - rate limit exceeded (after all retries exhausted)
Console.WriteLine($"Retry after {ex.RetryAfterSeconds} seconds");
}
catch (ApiException ex)
{
// Any other API error
Console.WriteLine($"{ex.StatusCode}: {ex.Message}");
}
Rate Limit Info
Every response includes rate limit information:
var result = await geo.GetCountryFromGpsAsync(40.7128, -74.0060);
Console.WriteLine($"Burst: {result.RateLimitInfo.BurstRemaining}/{result.RateLimitInfo.BurstLimit}");
Console.WriteLine($"Quota: {result.RateLimitInfo.QuotaRemaining}/{result.RateLimitInfo.QuotaLimit}");
Caching
The SDK caches geo responses in an LRU cache. Cache behavior:
- Only geo lookup methods are cached (deterministic, idempotent)
- Coordinates are rounded to 6 decimal places for cache key consistency
- Cache hit returns immediately without making an HTTP request
- TTL is checked lazily on access (expired entries are evicted on next read)
- LRU eviction when the cache reaches
MaxEntries
// First call - makes HTTP request
var r1 = await geo.GetCountryFromGpsAsync(34.0522, -118.2437);
// Second call - returns from cache (no HTTP request)
var r2 = await geo.GetCountryFromGpsAsync(34.0522, -118.2437);
// Clear cache manually
await geo.ClearCacheAsync();
To disable caching:
var geo = new GeolocationClient(new GeolocationClientOptions
{
ApiKey = "bnl_...",
Cache = new CacheOptions { Enabled = false },
});
Pluggable Cache
You can replace the built-in LRU cache with your own implementation by providing an IGeolocationCache:
public interface IGeolocationCache
{
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
Task SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken ct = default);
Task RemoveAsync(string key, CancellationToken ct = default);
Task ClearAsync(CancellationToken ct = default);
}
Pass your implementation via GeolocationClientOptions.CustomCache:
var geo = new GeolocationClient(new GeolocationClientOptions
{
ApiKey = "bnl_...",
CustomCache = new RedisGeolocationCache(redis),
});
When a custom cache is provided, the built-in LRU cache is bypassed entirely. If caching is disabled (Cache.Enabled = false) and no custom cache is set, the SDK logs a warning
every NoCacheWarningInterval requests (default 100) to remind you that caching is off.
Redis adapter example:
public class RedisGeolocationCache : IGeolocationCache
{
private readonly IConnectionMultiplexer _multiplexer;
private readonly IDatabase _db;
public RedisGeolocationCache(IConnectionMultiplexer redis)
{
_multiplexer = redis;
_db = redis.GetDatabase();
}
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
{
var value = await _db.StringGetAsync(key);
return value.HasValue ? JsonSerializer.Deserialize<T>(value!) : default;
}
public async Task SetAsync<T>(string key, T value, TimeSpan? ttl = null, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(value);
await _db.StringSetAsync(key, json, ttl);
}
public async Task RemoveAsync(string key, CancellationToken ct = default) => await _db.KeyDeleteAsync(key);
public async Task ClearAsync(CancellationToken ct = default)
{
var endpoints = _multiplexer.GetEndPoints();
foreach (var endpoint in endpoints)
{
var server = _multiplexer.GetServer(endpoint);
var keys = server.Keys(pattern: "GET:*");
foreach (var key in keys)
await _db.KeyDeleteAsync(key).ConfigureAwait(false);
}
}
}
For the complete working project, see the Redis Cache sample.
To clear the cache (works with both built-in and custom caches):
await geo.ClearCacheAsync();
Samples
| Sample | Description |
|---|---|
| Quickstart | Minimal usage with default LRU cache |
| Redis Cache | Custom Redis-backed cache adapter |
| GDPR Compliance | Web API with geolocation-driven GDPR policy |