diff --git a/IPinfo.Tests/IPApiPlusTest.cs b/IPinfo.Tests/IPApiPlusTest.cs
new file mode 100644
index 0000000..8abf287
--- /dev/null
+++ b/IPinfo.Tests/IPApiPlusTest.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+using IPinfo.Models;
+
+namespace IPinfo.Tests
+{
+ public class IPApiPlusTest
+ {
+ [Fact]
+ public void TestGetDetailsIPV4()
+ {
+ string ip = "8.8.8.8";
+ IPinfoClientPlus client = new IPinfoClientPlus.Builder()
+ .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
+ .Build();
+
+ IPResponsePlus actual = client.IPApi.GetDetails(ip);
+
+ Assert.Equal("8.8.8.8", actual.IP);
+ Assert.Equal("dns.google", actual.Hostname);
+ Assert.False(actual.Bogon);
+
+ // Geo assertions
+ Assert.NotNull(actual.Geo);
+ Assert.NotNull(actual.Geo.City);
+ Assert.NotNull(actual.Geo.Region);
+ Assert.NotNull(actual.Geo.RegionCode);
+ Assert.Equal("US", actual.Geo.CountryCode);
+ Assert.Equal("United States", actual.Geo.Country);
+ Assert.Equal("United States", actual.Geo.CountryName);
+ Assert.False(actual.Geo.IsEU);
+ Assert.NotNull(actual.Geo.Continent);
+ Assert.NotNull(actual.Geo.ContinentCode);
+ Assert.NotEqual(0, actual.Geo.Latitude);
+ Assert.NotEqual(0, actual.Geo.Longitude);
+ Assert.NotNull(actual.Geo.Timezone);
+ Assert.NotNull(actual.Geo.PostalCode);
+ Assert.Equal("🇺🇸", actual.Geo.CountryFlag.Emoji);
+ Assert.Equal("U+1F1FA U+1F1F8", actual.Geo.CountryFlag.Unicode);
+ Assert.Equal("https://cdn.ipinfo.io/static/images/countries-flags/US.svg", actual.Geo.CountryFlagURL);
+ Assert.Equal("USD", actual.Geo.CountryCurrency.Code);
+ Assert.Equal("$", actual.Geo.CountryCurrency.Symbol);
+ Assert.Equal("NA", actual.Geo.ContinentInfo.Code);
+ Assert.Equal("North America", actual.Geo.ContinentInfo.Name);
+
+ // AS assertions
+ Assert.NotNull(actual.As);
+ Assert.Equal("AS15169", actual.As.Asn);
+ Assert.NotNull(actual.As.Name);
+ Assert.NotNull(actual.As.Domain);
+ Assert.NotNull(actual.As.Type);
+
+ // Network flags
+ Assert.False(actual.IsAnonymous);
+ Assert.True(actual.IsAnycast);
+ Assert.True(actual.IsHosting);
+ Assert.False(actual.IsMobile);
+ Assert.False(actual.IsSatellite);
+
+ // Plus-specific fields (may be present based on token tier)
+ // These fields exist in the response structure
+ }
+
+ [Fact]
+ public void TestGetDetailsIPV6()
+ {
+ string ip = "2001:4860:4860::8888";
+ IPinfoClientPlus client = new IPinfoClientPlus.Builder()
+ .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
+ .Build();
+
+ IPResponsePlus actual = client.IPApi.GetDetails(ip);
+
+ Assert.Equal("2001:4860:4860::8888", actual.IP);
+
+ // Geo assertions
+ Assert.NotNull(actual.Geo);
+ Assert.Equal("US", actual.Geo.CountryCode);
+ Assert.Equal("United States", actual.Geo.Country);
+ Assert.NotNull(actual.Geo.City);
+ Assert.NotNull(actual.Geo.Region);
+
+ // AS assertions
+ Assert.NotNull(actual.As);
+ Assert.NotNull(actual.As.Asn);
+ Assert.NotNull(actual.As.Name);
+ Assert.NotNull(actual.As.Domain);
+
+ // Network flags
+ Assert.False(actual.IsAnonymous);
+ Assert.False(actual.IsMobile);
+ Assert.False(actual.IsSatellite);
+ }
+
+ [Fact]
+ public void TestBogonIPV4()
+ {
+ string ip = "127.0.0.1";
+ IPinfoClientPlus client = new IPinfoClientPlus.Builder()
+ .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
+ .Build();
+
+ IPResponsePlus actual = client.IPApi.GetDetails(ip);
+
+ Assert.Equal("127.0.0.1", actual.IP);
+ Assert.True(actual.Bogon);
+ }
+
+ [Fact]
+ public void TestBogonIPV6()
+ {
+ string ip = "2001:0:c000:200::0:255:1";
+ IPinfoClientPlus client = new IPinfoClientPlus.Builder()
+ .AccessToken(Environment.GetEnvironmentVariable("IPINFO_TOKEN"))
+ .Build();
+
+ IPResponsePlus actual = client.IPApi.GetDetails(ip);
+
+ Assert.Equal("2001:0:c000:200::0:255:1", actual.IP);
+ Assert.True(actual.Bogon);
+ }
+ }
+}
diff --git a/src/IPinfo/Apis/IPApiPlus.cs b/src/IPinfo/Apis/IPApiPlus.cs
new file mode 100644
index 0000000..4ef3334
--- /dev/null
+++ b/src/IPinfo/Apis/IPApiPlus.cs
@@ -0,0 +1,117 @@
+using System.Net;
+using System.Threading.Tasks;
+using System.Threading;
+
+using IPinfo.Utilities;
+using IPinfo.Http.Client;
+using IPinfo.Http.Request;
+using IPinfo.Models;
+using IPinfo.Http.Response;
+using IPinfo.Cache;
+
+namespace IPinfo.Apis
+{
+ ///
+ /// IPApiPlus.
+ ///
+ public sealed class IPApiPlus : BaseApi
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// httpClient.
+ /// token.
+ /// cacheHandler.
+ internal IPApiPlus(IHttpClient httpClient, string token, CacheHandler cacheHandler)
+ : base(httpClient, token, cacheHandler)
+ {
+ this.BaseUrl = "https://api.ipinfo.io/lookup/";
+ }
+
+ ///
+ /// Retrieves details of an IP address.
+ ///
+ /// The IP address of the user to retrieve details for.
+ /// Returns the Models.IPResponsePlus response from the API call.
+ public Models.IPResponsePlus GetDetails(
+ IPAddress ipAddress)
+ {
+ string ipString = ipAddress?.ToString();
+ return this.GetDetails(ipString);
+ }
+
+ ///
+ /// Retrieves details of an IP address.
+ ///
+ /// The IP address of the user to retrieve details for.
+ /// Returns the Models.IPResponsePlus response from the API call.
+ public Models.IPResponsePlus GetDetails(
+ string ipAddress = "")
+ {
+ Task t = this.GetDetailsAsync(ipAddress);
+ ApiHelper.RunTaskSynchronously(t);
+ return t.Result;
+ }
+
+ ///
+ /// Retrieves details of an IP address.
+ ///
+ /// The IP address of the user to retrieve details for.
+ /// Cancellation token if the request is cancelled.
+ /// Returns the Models.IPResponsePlus response from the API call.
+ public Task GetDetailsAsync(
+ IPAddress ipAddress,
+ CancellationToken cancellationToken = default)
+ {
+ string ipString = ipAddress?.ToString();
+ return this.GetDetailsAsync(ipString, cancellationToken);
+ }
+
+ ///
+ /// Retrieves details of an IP address.
+ ///
+ /// The IP address of the user to retrieve details for.
+ /// Cancellation token if the request is cancelled.
+ /// Returns the Models.IPResponsePlus response from the API call.
+ public async Task GetDetailsAsync(
+ string ipAddress = "",
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(ipAddress))
+ {
+ ipAddress = "";
+ }
+
+ // first check the data in the cache if cache is available
+ IPResponsePlus ipResponse = (IPResponsePlus)GetFromCache(ipAddress);
+ if (ipResponse != null)
+ {
+ return ipResponse;
+ }
+
+ if (BogonHelper.IsBogon(ipAddress))
+ {
+ ipResponse = new IPResponsePlus()
+ {
+ IP = ipAddress,
+ Bogon = true
+ };
+ return ipResponse;
+ }
+
+ // prepare the API call request to fetch the response.
+ HttpRequest httpRequest = this.CreateGetRequest(this.BaseUrl + ipAddress);
+ // invoke request and get response.
+ HttpStringResponse response = await this.GetClientInstance().ExecuteAsStringAsync(httpRequest, cancellationToken).ConfigureAwait(false);
+ HttpContext context = new HttpContext(httpRequest, response);
+
+ // handle errors defined at the API level.
+ this.ValidateResponse(context);
+
+ var responseModel = JsonHelper.ParseIPResponsePlus(response.Body);
+
+ SetInCache(ipAddress, responseModel);
+ return responseModel;
+ }
+ }
+}
diff --git a/src/IPinfo/IPinfoClientPlus.cs b/src/IPinfo/IPinfoClientPlus.cs
new file mode 100644
index 0000000..5279974
--- /dev/null
+++ b/src/IPinfo/IPinfoClientPlus.cs
@@ -0,0 +1,120 @@
+using System;
+
+using IPinfo.Http.Client;
+using IPinfo.Apis;
+using IPinfo.Cache;
+using IPinfo.Utilities;
+
+namespace IPinfo
+{
+ ///
+ /// The gateway for IPinfo Plus SDK. This class holds the configuration of the SDK.
+ ///
+ public sealed class IPinfoClientPlus
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly CacheHandler _cacheHandler;
+ private readonly Lazy _ipApi;
+
+ private IPinfoClientPlus(
+ string accessToken,
+ IHttpClient httpClient,
+ CacheHandler cacheHandler,
+ IHttpClientConfiguration httpClientConfiguration)
+ {
+ this._httpClient = httpClient;
+ this._cacheHandler = cacheHandler;
+ this.HttpClientConfiguration = httpClientConfiguration;
+
+ this._ipApi = new Lazy(
+ () => new IPApiPlus(this._httpClient, accessToken, cacheHandler));
+ }
+
+ ///
+ /// Gets IPApiPlus.
+ ///
+ public IPApiPlus IPApi => this._ipApi.Value;
+
+ ///
+ /// Gets the configuration of the Http Client associated with this client.
+ ///
+ public IHttpClientConfiguration HttpClientConfiguration { get; }
+
+ ///
+ /// Gets the configuration of the Http Client associated with this client.
+ ///
+ public ICache Cache { get => _cacheHandler?.Cache; }
+
+ ///
+ /// Builder class.
+ ///
+ public class Builder
+ {
+ private string _accessToken = "";
+ private HttpClientConfiguration.Builder _httpClientConfig = new HttpClientConfiguration.Builder();
+ private IHttpClient _httpClient;
+ private CacheHandler _cacheHandler = new CacheHandler();
+
+ ///
+ /// Sets credentials for BearerAuth.
+ ///
+ /// AccessToken.
+ /// Builder.
+ public Builder AccessToken(string accessToken)
+ {
+ this._accessToken = accessToken;
+ return this;
+ }
+
+ ///
+ /// Sets HttpClientConfig.
+ ///
+ /// Action.
+ /// Builder.
+ public Builder HttpClientConfig(Action action)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ action(this._httpClientConfig);
+ return this;
+ }
+
+ ///
+ /// Sets the ICache implementation for the Builder.
+ ///
+ /// ICache implementation. Pass null to disable the cache.
+ /// Builder.
+ public Builder Cache(ICache cache)
+ {
+ // Null is allowed here, which is being used to indicate that user do not want the cache.
+ if(cache == null)
+ {
+ this._cacheHandler = null;
+ }
+ else
+ {
+ this._cacheHandler = new CacheHandler(cache);
+ }
+ return this;
+ }
+
+ ///
+ /// Creates an object of the IPinfoClientPlus using the values provided for the builder.
+ ///
+ /// IPinfoClientPlus.
+ public IPinfoClientPlus Build()
+ {
+ this._httpClient = new HttpClientWrapper(this._httpClientConfig.Build());
+
+ return new IPinfoClientPlus(
+ _accessToken,
+ _httpClient,
+ _cacheHandler,
+ _httpClientConfig.Build());
+ }
+ }
+ }
+}
diff --git a/src/IPinfo/Models/IPResponsePlus.cs b/src/IPinfo/Models/IPResponsePlus.cs
new file mode 100644
index 0000000..e90d28b
--- /dev/null
+++ b/src/IPinfo/Models/IPResponsePlus.cs
@@ -0,0 +1,181 @@
+using System.Text.Json.Serialization;
+
+namespace IPinfo.Models
+{
+ public class IPResponsePlus
+ {
+ [JsonInclude]
+ public string IP { get; internal set; }
+
+ [JsonInclude]
+ public string Hostname { get; private set; }
+
+ [JsonInclude]
+ public bool Bogon { get; internal set; }
+
+ [JsonInclude]
+ public GeoPlus Geo { get; private set; }
+
+ [JsonPropertyName("as")]
+ [JsonInclude]
+ public ASPlus As { get; private set; }
+
+ [JsonInclude]
+ public MobilePlus Mobile { get; private set; }
+
+ [JsonInclude]
+ public AnonymousPlus Anonymous { get; private set; }
+
+ [JsonPropertyName("is_anonymous")]
+ [JsonInclude]
+ public bool IsAnonymous { get; private set; }
+
+ [JsonPropertyName("is_anycast")]
+ [JsonInclude]
+ public bool IsAnycast { get; private set; }
+
+ [JsonPropertyName("is_hosting")]
+ [JsonInclude]
+ public bool IsHosting { get; private set; }
+
+ [JsonPropertyName("is_mobile")]
+ [JsonInclude]
+ public bool IsMobile { get; private set; }
+
+ [JsonPropertyName("is_satellite")]
+ [JsonInclude]
+ public bool IsSatellite { get; private set; }
+
+ [JsonInclude]
+ public Abuse Abuse { get; private set; }
+
+ [JsonInclude]
+ public Company Company { get; private set; }
+
+ [JsonInclude]
+ public Privacy Privacy { get; private set; }
+
+ [JsonInclude]
+ public DomainsList Domains { get; private set; }
+ }
+
+ public class GeoPlus
+ {
+ [JsonInclude]
+ public string City { get; private set; }
+
+ [JsonInclude]
+ public string Region { get; private set; }
+
+ [JsonPropertyName("region_code")]
+ [JsonInclude]
+ public string RegionCode { get; private set; }
+
+ [JsonInclude]
+ public string Country { get; private set; }
+
+ [JsonPropertyName("country_code")]
+ [JsonInclude]
+ public string CountryCode { get; private set; }
+
+ public string CountryName { get; internal set; }
+
+ public bool IsEU { get; internal set; }
+
+ public CountryFlag CountryFlag { get; internal set; }
+
+ public string CountryFlagURL { get; internal set; }
+
+ public CountryCurrency CountryCurrency { get; internal set; }
+
+ [JsonInclude]
+ public string Continent { get; private set; }
+
+ [JsonPropertyName("continent_code")]
+ [JsonInclude]
+ public string ContinentCode { get; private set; }
+
+ public Continent ContinentInfo { get; internal set; }
+
+ [JsonInclude]
+ public double Latitude { get; private set; }
+
+ [JsonInclude]
+ public double Longitude { get; private set; }
+
+ [JsonInclude]
+ public string Timezone { get; private set; }
+
+ [JsonPropertyName("postal_code")]
+ [JsonInclude]
+ public string PostalCode { get; private set; }
+
+ [JsonPropertyName("dma_code")]
+ [JsonInclude]
+ public string DmaCode { get; private set; }
+
+ [JsonPropertyName("geoname_id")]
+ [JsonInclude]
+ public string GeonameId { get; private set; }
+
+ [JsonInclude]
+ public int Radius { get; private set; }
+
+ [JsonPropertyName("last_changed")]
+ [JsonInclude]
+ public string LastChanged { get; private set; }
+ }
+
+ public class ASPlus
+ {
+ [JsonInclude]
+ public string Asn { get; private set; }
+
+ [JsonInclude]
+ public string Name { get; private set; }
+
+ [JsonInclude]
+ public string Domain { get; private set; }
+
+ [JsonInclude]
+ public string Type { get; private set; }
+
+ [JsonPropertyName("last_changed")]
+ [JsonInclude]
+ public string LastChanged { get; private set; }
+ }
+
+ public class MobilePlus
+ {
+ [JsonInclude]
+ public string Name { get; private set; }
+
+ [JsonInclude]
+ public string Mcc { get; private set; }
+
+ [JsonInclude]
+ public string Mnc { get; private set; }
+ }
+
+ public class AnonymousPlus
+ {
+ [JsonPropertyName("is_proxy")]
+ [JsonInclude]
+ public bool IsProxy { get; private set; }
+
+ [JsonPropertyName("is_relay")]
+ [JsonInclude]
+ public bool IsRelay { get; private set; }
+
+ [JsonPropertyName("is_tor")]
+ [JsonInclude]
+ public bool IsTor { get; private set; }
+
+ [JsonPropertyName("is_vpn")]
+ [JsonInclude]
+ public bool IsVpn { get; private set; }
+
+ [JsonInclude]
+ public string Name { get; private set; }
+ }
+}
diff --git a/src/IPinfo/Utilities/JsonHelper.cs b/src/IPinfo/Utilities/JsonHelper.cs
index e624fa8..47f5149 100644
--- a/src/IPinfo/Utilities/JsonHelper.cs
+++ b/src/IPinfo/Utilities/JsonHelper.cs
@@ -133,5 +133,27 @@ internal static IPResponseCore ParseIPResponseCore(string response) {
return responseModel;
}
+
+ ///
+ /// IPResponsePlus object with extra manual parsing.
+ ///
+ /// The json string to be parsed.
+ /// The deserialized IPResponsePlus object with extra parsing for geo object country enrichment being done.
+ internal static IPResponsePlus ParseIPResponsePlus(string response) {
+ IPResponsePlus responseModel = JsonHelper.Deserialize(response);
+
+ if (responseModel.Geo != null && !String.IsNullOrEmpty(responseModel.Geo.CountryCode))
+ {
+ responseModel.Geo.CountryName = CountryHelper.GetCountry(responseModel.Geo.CountryCode);
+ responseModel.Geo.IsEU = CountryHelper.IsEU(responseModel.Geo.CountryCode);
+ responseModel.Geo.CountryFlag = CountryHelper.GetCountryFlag(responseModel.Geo.CountryCode);
+ responseModel.Geo.CountryCurrency = CountryHelper.GetCountryCurrency(responseModel.Geo.CountryCode);
+ responseModel.Geo.ContinentInfo = CountryHelper.GetContinent(responseModel.Geo.CountryCode);
+ responseModel.Geo.CountryFlagURL = CountryFlagURL + responseModel.Geo.CountryCode + ".svg";
+ }
+
+ return responseModel;
+ }
}
+
}