Skip to main content

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

OptionDefaultDescription
ApiKey(required)Your API key starting with bnl_
BaseUrlhttps://api.boxesnlines.comAPI base URL
TimeoutSeconds30Request timeout in seconds
MaxRetries3Max retry attempts on 429/5xx
Cache.EnabledtrueEnable in-memory LRU cache
Cache.DefaultTtl5 minutesCache entry time-to-live
Cache.MaxEntries1000Maximum cache entries
NoCacheWarningInterval100Emit 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

SampleDescription
QuickstartMinimal usage with default LRU cache
Redis CacheCustom Redis-backed cache adapter
GDPR ComplianceWeb API with geolocation-driven GDPR policy