From 25951520dc695c16d10bbf753715823f5b0b265d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:50:37 +0000 Subject: [PATCH 1/7] Populate IPLookupResult.Languages from the response System.Text.Json never sets a collection property that has a non-public setter, so Languages has deserialised as an empty list since the move to System.Text.Json. Give it a public setter, as Blocklists already has, and cover it with the existing fixtures. Co-Authored-By: Claude Fable 5.1 --- src/IPData/Models/IPLookupResult.cs | 6 ++- .../Http/Serializer/JsonSerializerTests.cs | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/IPData/Models/IPLookupResult.cs b/src/IPData/Models/IPLookupResult.cs index ed31be0..0235761 100644 --- a/src/IPData/Models/IPLookupResult.cs +++ b/src/IPData/Models/IPLookupResult.cs @@ -78,8 +78,12 @@ public class IPLookupResult [JsonPropertyName("emoji_unicode")] public string EmojiUnicode { get; set; } + /// + /// The languages spoken where the address is located. The setter is public because + /// System.Text.Json does not populate collection properties that have a non-public setter. + /// [JsonPropertyName("languages")] - public List Languages { get; private set; } = new List(); + public List Languages { get; set; } = new List(); [JsonPropertyName("currency")] public Currency Currency { get; set; } diff --git a/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs b/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs index 19c90a7..a249311 100644 --- a/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs +++ b/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs @@ -23,6 +23,47 @@ public void Deserialize_WhenCalled_ReturnedIPLookupResult(string json) actual.Should().NotBeNull(); } + [Theory] + [MemberData(nameof(TestDataSource.IPLookupResultData), MemberType = typeof(TestDataSource))] + public void Deserialize_WhenCalled_ReadsLanguages(string json) + { + // Act + var actual = _sut.Deserialize(json); + + // Assert + var language = actual.Languages.Should().ContainSingle().Which; + language.Name.Should().Be("Ukrainian"); + language.Native.Should().Be("\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430"); + language.Code.Should().Be("uk"); + } + + [Theory] + [MemberData(nameof(TestDataSource.AnycastData), MemberType = typeof(TestDataSource))] + public void Deserialize_AnycastResponse_ReadsLanguages(string json) + { + // Act + var actual = _sut.Deserialize(json); + + // Assert + var language = actual.Languages.Should().ContainSingle().Which; + language.Name.Should().Be("English"); + language.Native.Should().Be("English"); + language.Code.Should().Be("en"); + } + + [Fact] + public void Deserialize_WhenLanguagesAbsent_ReadsEmptyList() + { + // Arrange + var json = "{\"ip\":\"1.1.1.1\"}"; + + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.Languages.Should().NotBeNull().And.BeEmpty(); + } + [Fact] public void Deserialize_CountAsString_ParsedCorrectly() { @@ -147,6 +188,7 @@ public void Deserialize_BulkResponse_CarriesNewFieldsPerItem(string json) actual[1].IsAnycast.Should().BeTrue(); actual[1].IsSatellite.Should().BeFalse(); actual[1].Threat.Blocklists[0].DaysSeen.Should().Be(1); + actual[1].Languages.Should().ContainSingle().Which.Code.Should().Be("en"); actual[2].IsSatellite.Should().BeFalse(); actual[2].IsAnycast.Should().BeFalse(); actual[2].Threat.Blocklists[2].DaysSeen.Should().BeNull(); From 3e91b8c19db926a63ccd49881181c6cb96d9b2c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:52:18 +0000 Subject: [PATCH 2/7] Add Threat.IsVpn and Threat.Scores The threat object of a response carries is_vpn and a scores object (vpn_score, proxy_score, threat_score and trust_score), which the model dropped. Add IsVpn (bool?) and Scores (a new ThreatScores class with int? members) so both are read; null means the response did not include the value. Document the fields in the README and the sample. Co-Authored-By: Claude Fable 5.1 --- .github/README.md | 3 + samples/IPData.Lookup/Program.cs | 1 + src/IPData/Models/Threat.cs | 13 +++ src/IPData/Models/ThreatScores.cs | 23 ++++ .../Http/Serializer/JsonSerializerTests.cs | 101 ++++++++++++++++++ 5 files changed, 141 insertions(+) create mode 100644 src/IPData/Models/ThreatScores.cs diff --git a/.github/README.md b/.github/README.md index d636259..431554e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -134,6 +134,7 @@ var client = new IPDataClient("API_KEY"); var threatInfo = await client.Threat("69.78.70.144"); Console.WriteLine($"Threat is Tor: {threatInfo.IsTor}"); +Console.WriteLine($"Threat is VPN: {threatInfo.IsVpn}, trust score: {threatInfo.Scores?.TrustScore}"); ``` ### Response fields @@ -147,6 +148,8 @@ Console.WriteLine($"Threat is Tor: {threatInfo.IsTor}"); | `threat.blocklists[].first_seen` | `BlocklistInfo.FirstSeen` | First day the address was seen on that blocklist, a `YYYY-MM-DD` (UTC) string; `null` when the feed does not report it. | | `threat.blocklists[].last_seen` | `BlocklistInfo.LastSeen` | Last day the address was seen on that blocklist, a `YYYY-MM-DD` (UTC) string; `null` when the feed does not report it. | | `threat.blocklists[].days_seen` | `BlocklistInfo.DaysSeen` | Number of days the address was seen on that blocklist (`int?`); `null` when the feed does not report it. | +| `threat.is_vpn` | `Threat.IsVpn` | `true` when the address is a VPN endpoint (`bool?`); `null` when the response does not include the key. | +| `threat.scores` | `Threat.Scores` | A `ThreatScores` object with `VpnScore`, `ProxyScore`, `ThreatScore` and `TrustScore` (`int?`, from `vpn_score`, `proxy_score`, `threat_score` and `trust_score`). `Scores` is `null` when the response does not include the object, and a score is `null` when the object does not include it. | Both flags can be requested on their own or as part of a field selection: diff --git a/samples/IPData.Lookup/Program.cs b/samples/IPData.Lookup/Program.cs index e2696e7..1272287 100644 --- a/samples/IPData.Lookup/Program.cs +++ b/samples/IPData.Lookup/Program.cs @@ -45,6 +45,7 @@ public static async Task Main() // Get threat info from IP var threatInfo = await client.Threat("69.78.70.144"); Console.WriteLine($"Threat is Tor: {threatInfo.IsTor}"); + Console.WriteLine($"Threat is VPN: {threatInfo.IsVpn}, trust score: {threatInfo.Scores?.TrustScore}"); // Get IP data for multiple IPs var ipInfoList = await client.Lookup(new[] { "1.1.1.1", "2.2.2.2", "3.3.3.3" }); diff --git a/src/IPData/Models/Threat.cs b/src/IPData/Models/Threat.cs index 75f031e..958616b 100644 --- a/src/IPData/Models/Threat.cs +++ b/src/IPData/Models/Threat.cs @@ -8,6 +8,12 @@ public class Threat [JsonPropertyName("is_tor")] public bool? IsTor { get; set; } + /// + /// Whether the address is a VPN endpoint, or null when the response does not include the key. + /// + [JsonPropertyName("is_vpn")] + public bool? IsVpn { get; set; } + [JsonPropertyName("is_proxy")] public bool? IsProxy { get; set; } @@ -38,5 +44,12 @@ public class Threat /// [JsonPropertyName("blocklists")] public List Blocklists { get; set; } = new List(); + + /// + /// The scores the API reports for the address (vpn_score, proxy_score, threat_score + /// and trust_score), or null when the response does not include the scores object. + /// + [JsonPropertyName("scores")] + public ThreatScores Scores { get; set; } } } diff --git a/src/IPData/Models/ThreatScores.cs b/src/IPData/Models/ThreatScores.cs new file mode 100644 index 0000000..4aa6d94 --- /dev/null +++ b/src/IPData/Models/ThreatScores.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace IPData.Models +{ + /// + /// The scores object of a threat response. Each score is null when the response does not + /// include it. + /// + public class ThreatScores + { + [JsonPropertyName("vpn_score")] + public int? VpnScore { get; set; } + + [JsonPropertyName("proxy_score")] + public int? ProxyScore { get; set; } + + [JsonPropertyName("threat_score")] + public int? ThreatScore { get; set; } + + [JsonPropertyName("trust_score")] + public int? TrustScore { get; set; } + } +} diff --git a/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs b/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs index a249311..43714d9 100644 --- a/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs +++ b/test/Unit/IPData.Tests/Http/Serializer/JsonSerializerTests.cs @@ -174,6 +174,103 @@ public void Deserialize_VpnResponse_LeavesFlagsFalseAndBlocklistDatesNullWhenAbs undated.DaysSeen.Should().BeNull(); } + [Theory] + [MemberData(nameof(TestDataSource.VpnData), MemberType = typeof(TestDataSource))] + public void Deserialize_VpnResponse_ReadsIsVpnAndScores(string json) + { + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.Threat.IsVpn.Should().BeTrue(); + actual.Threat.Scores.Should().NotBeNull(); + actual.Threat.Scores.VpnScore.Should().Be(100); + actual.Threat.Scores.ProxyScore.Should().Be(99); + actual.Threat.Scores.ThreatScore.Should().Be(100); + actual.Threat.Scores.TrustScore.Should().Be(0); + } + + [Theory] + [MemberData(nameof(TestDataSource.SatelliteData), MemberType = typeof(TestDataSource))] + public void Deserialize_SatelliteResponse_ReadsIsVpnFalseAndScores(string json) + { + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.Threat.IsVpn.Should().BeFalse(); + actual.Threat.Scores.VpnScore.Should().Be(0); + actual.Threat.Scores.ProxyScore.Should().Be(1); + actual.Threat.Scores.ThreatScore.Should().Be(8); + actual.Threat.Scores.TrustScore.Should().Be(97); + } + + [Theory] + [MemberData(nameof(TestDataSource.IPLookupResultData), MemberType = typeof(TestDataSource))] + public void Deserialize_WhenIsVpnAndScoresAbsent_ReadNull(string json) + { + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.Threat.IsVpn.Should().BeNull(); + actual.Threat.Scores.Should().BeNull(); + } + + [Fact] + public void Deserialize_WhenScoresPartiallyPresent_LeavesMissingScoresNull() + { + // Arrange + var json = "{\"threat\":{\"scores\":{\"trust_score\":42}}}"; + + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.Threat.Scores.TrustScore.Should().Be(42); + actual.Threat.Scores.VpnScore.Should().BeNull(); + actual.Threat.Scores.ProxyScore.Should().BeNull(); + actual.Threat.Scores.ThreatScore.Should().BeNull(); + } + + [Fact] + public void Deserialize_ThreatResponse_ReadsIsVpnAndScores() + { + // Arrange: the body of a GET /{ip}/threat request + var json = "{\"is_tor\":false,\"is_vpn\":true,\"is_proxy\":false,\"is_threat\":true,\"blocklists\":[]," + + "\"scores\":{\"vpn_score\":100,\"proxy_score\":99,\"threat_score\":100,\"trust_score\":0}}"; + + // Act + var actual = _sut.Deserialize(json); + + // Assert + actual.IsVpn.Should().BeTrue(); + actual.IsThreat.Should().BeTrue(); + actual.Blocklists.Should().BeEmpty(); + actual.Scores.VpnScore.Should().Be(100); + actual.Scores.ProxyScore.Should().Be(99); + actual.Scores.ThreatScore.Should().Be(100); + actual.Scores.TrustScore.Should().Be(0); + } + + [Fact] + public void SerializeThreat_WhenCalled_RoundTripsIsVpnAndScores() + { + // Arrange + var threat = new Threat + { + IsVpn = true, + Scores = new ThreatScores { VpnScore = 100, ProxyScore = 99, ThreatScore = 100, TrustScore = 0 } + }; + + // Act + var actual = _sut.Deserialize(_sut.Serialize(threat)); + + // Assert + actual.IsVpn.Should().BeTrue(); + actual.Scores.Should().BeEquivalentTo(threat.Scores); + } + [Theory] [MemberData(nameof(TestDataSource.BulkData), MemberType = typeof(TestDataSource))] public void Deserialize_BulkResponse_CarriesNewFieldsPerItem(string json) @@ -192,6 +289,10 @@ public void Deserialize_BulkResponse_CarriesNewFieldsPerItem(string json) actual[2].IsSatellite.Should().BeFalse(); actual[2].IsAnycast.Should().BeFalse(); actual[2].Threat.Blocklists[2].DaysSeen.Should().BeNull(); + actual[0].Threat.IsVpn.Should().BeFalse(); + actual[0].Threat.Scores.TrustScore.Should().Be(97); + actual[2].Threat.IsVpn.Should().BeTrue(); + actual[2].Threat.Scores.TrustScore.Should().Be(0); } [Fact] From a218bab486ab8a1a10149beedd2471fd86dbbada Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:53:42 +0000 Subject: [PATCH 3/7] Send User-Agent and Accept headers on every request Requests carried no User-Agent, so the API could not tell which client library made them, and no Accept header. CreateRequest now adds "User-Agent: ipdata-dotnet/" and "Accept: application/json" on both target frameworks. The version is the package version read from the assembly's informational version at run time (without the "+commit" build metadata), so no new dependency is needed. Co-Authored-By: Claude Fable 5.1 --- .github/README.md | 1 + src/IPData/IPDataClient.cs | 45 +++++++++++- test/Unit/IPData.Tests/IPDataClientTests.cs | 77 +++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 431554e..94dd3fe 100644 --- a/.github/README.md +++ b/.github/README.md @@ -166,6 +166,7 @@ An `IPDataClient` keeps a persistent HTTPS connection pool for as long as it liv - **Shared default pool.** Clients constructed without an `HttpClient` (`new IPDataClient("API_KEY")`, with or without a base URL) share one process-wide `HttpClient`. It lives for the lifetime of the process and is never disposed, so there is nothing to close. On .NET 8+ it keeps idle connections for 60 seconds, recycles every connection after 5 minutes and sends HTTP/2 keep-alive pings every 30 seconds. - **HTTP/2 on .NET 8+.** The `net8.0` build asks for HTTP/2 on every request (`HttpVersionPolicy.RequestVersionOrLower`) and falls back to HTTP/1.1 automatically when the server or a proxy does not negotiate it. The `netstandard2.0` build, used by .NET Framework and other .NET Standard consumers, never asks for HTTP/2 and relies on HTTP/1.1 keep-alive. - **Bring your own `HttpClient`.** Pass an `HttpClient` to send every request through it, for example one managed by `IHttpClientFactory` (see [Dependency Injection](#dependency-injection)). It can be combined with a custom base URL through the `IPDataClient(string apiKey, HttpClient httpClient, Uri baseUrl)` constructor. You own that client's lifetime; the library never disposes it. The same applies to a custom `IHttpClient` implementation. +- **Request headers.** Every request carries `User-Agent: ipdata-dotnet/` (the package version) and `Accept: application/json`. ```csharp // One client for the whole application; reuse it for every lookup. diff --git a/src/IPData/IPDataClient.cs b/src/IPData/IPDataClient.cs index 5d96cab..c6bd6b5 100644 --- a/src/IPData/IPDataClient.cs +++ b/src/IPData/IPDataClient.cs @@ -3,6 +3,8 @@ using System.Globalization; using System.Linq.Expressions; using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -23,7 +25,8 @@ namespace IPData /// client that lives as long as the process does and is never disposed; an or /// passed in stays owned by the caller. The class holds no per-request state /// and is safe to use from several threads at once. On .NET 5 and later every request asks for HTTP/2 - /// and falls back to HTTP/1.1 when the server does not negotiate it. + /// and falls back to HTTP/1.1 when the server does not negotiate it. Every request carries a + /// User-Agent header naming the library and its version and an Accept: application/json header. /// public class IPDataClient : IIPDataClient { @@ -32,6 +35,15 @@ public class IPDataClient : IIPDataClient /// The exception factory private static readonly IApiExceptionFactory _exceptionFactory = new ApiExceptionFactory(); + /// The product token sent in the User-Agent header of every request. + internal const string UserAgentProduct = "ipdata-dotnet"; + + /// + /// The User-Agent value sent with every request, ipdata-dotnet/<version>, where the + /// version is the package version of this assembly. It is read once, when the type is initialized. + /// + internal static readonly ProductInfoHeaderValue UserAgent = CreateUserAgent(); + /// The HTTP client private readonly IHttpClient _httpClient; @@ -215,6 +227,7 @@ public async Task Threat(string ip) /// Creates the request message for an API call. /// + /// Every request carries the library's User-Agent and an Accept: application/json header. /// On .NET 5 and later the request asks for HTTP/2 and lets the handler fall back to HTTP/1.1 /// (HttpVersionPolicy.RequestVersionOrLower). The .NET Standard 2.0 build leaves the default version /// alone, because .NET Framework rejects requests that ask for HTTP/2. @@ -225,6 +238,8 @@ public async Task Threat(string ip) private static HttpRequestMessage CreateRequest(HttpMethod method, Uri url) { var request = new HttpRequestMessage(method, url); + request.Headers.UserAgent.Add(UserAgent); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); #if NET5_0_OR_GREATER request.Version = System.Net.HttpVersion.Version20; request.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower; @@ -232,6 +247,34 @@ private static HttpRequestMessage CreateRequest(HttpMethod method, Uri url) return request; } + /// + /// Builds the User-Agent value from the assembly's informational version, which is the package + /// version without the +commit build metadata the SDK appends to it. Falls back to the assembly + /// version when the informational version is missing or is not a valid header token. + /// + /// The ipdata-dotnet/<version> product token. + private static ProductInfoHeaderValue CreateUserAgent() + { + var assembly = typeof(IPDataClient).Assembly; + var version = assembly.GetCustomAttribute()?.InformationalVersion; + + if (!string.IsNullOrWhiteSpace(version)) + { + var metadataStart = version.IndexOf('+'); + if (metadataStart >= 0) + { + version = version.Substring(0, metadataStart); + } + + if (ProductInfoHeaderValue.TryParse($"{UserAgentProduct}/{version}", out var userAgent)) + { + return userAgent; + } + } + + return new ProductInfoHeaderValue(UserAgentProduct, assembly.GetName().Version?.ToString(3) ?? "0.0.0"); + } + /// Sends the request asynchronous and disposes the request and the response once the body has been read. /// The HTTP client. /// The request. diff --git a/test/Unit/IPData.Tests/IPDataClientTests.cs b/test/Unit/IPData.Tests/IPDataClientTests.cs index 46e3fc9..eab03dc 100644 --- a/test/Unit/IPData.Tests/IPDataClientTests.cs +++ b/test/Unit/IPData.Tests/IPDataClientTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Reflection; using System.Text; using System.Threading.Tasks; using AutoFixture.Xunit2; @@ -82,6 +83,79 @@ private static HttpResponseMessage OkJson(string json) => Content = new StringContent(json, Encoding.UTF8, "application/json") }; + /// The package version of the library: its informational version without the "+commit" build metadata. + private static string ExpectedLibraryVersion + { + get + { + var version = typeof(IPDataClient).Assembly + .GetCustomAttribute() + .InformationalVersion; + var metadataStart = version.IndexOf('+'); + return metadataStart < 0 ? version : version.Substring(0, metadataStart); + } + } + + private static string ExpectedUserAgent => $"ipdata-dotnet/{ExpectedLibraryVersion}"; + + [Fact] + public void UserAgent_ShouldNameTheLibraryAndItsPackageVersion() + { + // Act + var userAgent = IPDataClient.UserAgent; + + // Assert + userAgent.Product.Name.Should().Be("ipdata-dotnet"); + userAgent.Product.Version.Should().Be(ExpectedLibraryVersion); + userAgent.Product.Version.Should().StartWith(typeof(IPDataClient).Assembly.GetName().Version.ToString(3)); + userAgent.ToString().Should().Be(ExpectedUserAgent); + } + + [Fact] + public async Task Lookup_WhenCalled_ShouldSendUserAgentAndAcceptHeaders() + { + // Arrange + var requests = new List(); + var httpClient = new Mock(); + httpClient + .Setup(x => x.SendAsync(It.IsAny())) + .Callback(requests.Add) + .Returns(() => Task.FromResult(OkJson(LookupJson))); + + var sut = new IPDataClient("test-api-key", httpClient.Object); + + // Act + await sut.Lookup("8.8.8.8").ConfigureAwait(false); + + // Assert + var request = requests.Should().ContainSingle().Which; + request.Headers.UserAgent.ToString().Should().Be(ExpectedUserAgent); + request.Headers.Accept.Should().ContainSingle().Which.MediaType.Should().Be("application/json"); + } + + [Fact] + public async Task Lookup_WhenCalledWithIpList_ShouldSendUserAgentAndAcceptHeaders() + { + // Arrange + var requests = new List(); + var httpClient = new Mock(); + httpClient + .Setup(x => x.SendAsync(It.IsAny())) + .Callback(requests.Add) + .Returns(() => Task.FromResult(OkJson(BulkJson))); + + var sut = new IPDataClient("test-api-key", httpClient.Object); + + // Act + await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }).ConfigureAwait(false); + + // Assert + var request = requests.Should().ContainSingle().Which; + request.Method.Should().Be(HttpMethod.Post); + request.Headers.UserAgent.ToString().Should().Be(ExpectedUserAgent); + request.Headers.Accept.Should().ContainSingle().Which.MediaType.Should().Be("application/json"); + } + [Theory, AutoMoqData] public void IPDataClient_WhenCreatedWithHttpClientAndBaseUrl_ShouldCreateClient( string apiKey) @@ -227,9 +301,12 @@ public async Task Lookup_WhenCalledRepeatedlyOnOneInstance_ShouldSendEveryReques await sut.Threat("8.8.8.8").ConfigureAwait(false); // Assert + var expectedUserAgent = ExpectedUserAgent; handler.Requests.Should().HaveCount(3); handler.Requests.Should().OnlyContain(r => r.Version == HttpVersion.Version20); handler.Requests.Should().OnlyContain(r => r.VersionPolicy == HttpVersionPolicy.RequestVersionOrLower); + handler.Requests.Should().OnlyContain(r => r.Headers.UserAgent.ToString() == expectedUserAgent); + handler.Requests.Should().OnlyContain(r => r.Headers.Accept.ToString() == "application/json"); } } From 3554ed5e4a25a08f890e9c884e248c2fd7ccad64 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:54:41 +0000 Subject: [PATCH 4/7] Mark the serialization constructors obsolete on .NET 8 Formatter-based serialization is obsolete on .NET 8 and later, and the protected (SerializationInfo, StreamingContext) constructors of ApiException and its subclasses produced SYSLIB0051 warnings in the net8.0 build. The constructors stay on every target so the public shape of the types does not change; on net8.0 they are marked obsolete with the same SYSLIB0051 diagnostic id, which silences the warning in the library and passes it on to any code that still calls them. The netstandard2.0 build is unchanged. Co-Authored-By: Claude Fable 5.1 --- src/IPData/Exceptions/ApiException.cs | 20 +++++++++++ src/IPData/Exceptions/BadRequestException.cs | 8 +++++ src/IPData/Exceptions/ForbiddenException.cs | 8 +++++ .../Exceptions/UnauthorizedException.cs | 8 +++++ .../Exceptions/ApiExceptionTests.cs | 33 ++++++++++++++++++- 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/IPData/Exceptions/ApiException.cs b/src/IPData/Exceptions/ApiException.cs index e4424b5..618c790 100644 --- a/src/IPData/Exceptions/ApiException.cs +++ b/src/IPData/Exceptions/ApiException.cs @@ -10,6 +10,18 @@ namespace IPData.Exceptions [Serializable] public class ApiException : Exception { +#if NET8_0_OR_GREATER + /// The message of the obsoletion that .NET 8 and later put on formatter-based serialization. + internal const string SerializationObsoleteMessage = + "This API supports obsolete formatter-based serialization. It should not be called or extended by application code."; + + /// The diagnostic id of that obsoletion. + internal const string SerializationDiagnosticId = "SYSLIB0051"; + + /// The documentation link of that obsoletion. + internal const string SerializationUrlFormat = "https://aka.ms/dotnet-warnings/{0}"; +#endif + private static readonly ISerializer _serializer = new JsonSerializer(); [NonSerialized] @@ -45,7 +57,15 @@ protected ApiException(HttpStatusCode statusCode, ApiError apiError, Exception i ApiError = apiError ?? throw new ArgumentNullException(nameof(apiError), "ApiError can't be null"); } + /// Initializes a new instance of the class with serialized data. + /// + /// Formatter-based serialization is obsolete on .NET 8 and later (SYSLIB0051). The constructor is kept on + /// every target so the type has the same shape everywhere, and carries that obsoletion on .NET 8 and later. + /// [ExcludeFromCodeCoverage] +#if NET8_0_OR_GREATER + [Obsolete(SerializationObsoleteMessage, DiagnosticId = SerializationDiagnosticId, UrlFormat = SerializationUrlFormat)] +#endif protected ApiException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { diff --git a/src/IPData/Exceptions/BadRequestException.cs b/src/IPData/Exceptions/BadRequestException.cs index d00c112..bf45f82 100644 --- a/src/IPData/Exceptions/BadRequestException.cs +++ b/src/IPData/Exceptions/BadRequestException.cs @@ -23,7 +23,15 @@ public BadRequestException(string responseContent, Exception innerException) { } + /// Initializes a new instance of the class with serialized data. + /// + /// Formatter-based serialization is obsolete on .NET 8 and later (SYSLIB0051). The constructor is kept on + /// every target so the type has the same shape everywhere, and carries that obsoletion on .NET 8 and later. + /// [ExcludeFromCodeCoverage] +#if NET8_0_OR_GREATER + [Obsolete(SerializationObsoleteMessage, DiagnosticId = SerializationDiagnosticId, UrlFormat = SerializationUrlFormat)] +#endif protected BadRequestException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { diff --git a/src/IPData/Exceptions/ForbiddenException.cs b/src/IPData/Exceptions/ForbiddenException.cs index f95db08..5f09cc7 100644 --- a/src/IPData/Exceptions/ForbiddenException.cs +++ b/src/IPData/Exceptions/ForbiddenException.cs @@ -23,7 +23,15 @@ public ForbiddenException(string responseContent, Exception innerException) { } + /// Initializes a new instance of the class with serialized data. + /// + /// Formatter-based serialization is obsolete on .NET 8 and later (SYSLIB0051). The constructor is kept on + /// every target so the type has the same shape everywhere, and carries that obsoletion on .NET 8 and later. + /// [ExcludeFromCodeCoverage] +#if NET8_0_OR_GREATER + [Obsolete(SerializationObsoleteMessage, DiagnosticId = SerializationDiagnosticId, UrlFormat = SerializationUrlFormat)] +#endif protected ForbiddenException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { diff --git a/src/IPData/Exceptions/UnauthorizedException.cs b/src/IPData/Exceptions/UnauthorizedException.cs index ede2412..6259614 100644 --- a/src/IPData/Exceptions/UnauthorizedException.cs +++ b/src/IPData/Exceptions/UnauthorizedException.cs @@ -23,7 +23,15 @@ public UnauthorizedException(string message, Exception innerException) { } + /// Initializes a new instance of the class with serialized data. + /// + /// Formatter-based serialization is obsolete on .NET 8 and later (SYSLIB0051). The constructor is kept on + /// every target so the type has the same shape everywhere, and carries that obsoletion on .NET 8 and later. + /// [ExcludeFromCodeCoverage] +#if NET8_0_OR_GREATER + [Obsolete(SerializationObsoleteMessage, DiagnosticId = SerializationDiagnosticId, UrlFormat = SerializationUrlFormat)] +#endif protected UnauthorizedException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { diff --git a/test/Unit/IPData.Tests/Exceptions/ApiExceptionTests.cs b/test/Unit/IPData.Tests/Exceptions/ApiExceptionTests.cs index 2a742a4..9f197df 100644 --- a/test/Unit/IPData.Tests/Exceptions/ApiExceptionTests.cs +++ b/test/Unit/IPData.Tests/Exceptions/ApiExceptionTests.cs @@ -1,4 +1,8 @@ -using System.Globalization; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Runtime.Serialization; using FluentAssertions; using IPData.Exceptions; using Xunit; @@ -78,5 +82,32 @@ public void ApiExceptionException_WhenCreateWithValidJsonContent_ShouldBeDeseria // Assert sut.Message.Should().Be(message); } + + public static IEnumerable ExceptionTypes() + { + yield return new object[] { typeof(ApiException) }; + yield return new object[] { typeof(BadRequestException) }; + yield return new object[] { typeof(ForbiddenException) }; + yield return new object[] { typeof(UnauthorizedException) }; + } + + [Theory] + [MemberData(nameof(ExceptionTypes))] + public void Exception_ShouldKeepTheProtectedSerializationConstructorAndMarkItObsolete(Type exceptionType) + { + // Act + var constructor = exceptionType.GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(SerializationInfo), typeof(StreamingContext) }, + null); + + // Assert + constructor.Should().NotBeNull(); + constructor.IsFamily.Should().BeTrue(); + var obsolete = constructor.GetCustomAttribute(); + obsolete.Should().NotBeNull(); + obsolete.DiagnosticId.Should().Be("SYSLIB0051"); + } } } From d412f520a9e78d264057c6943c47ed9d25ea1cb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:54:57 +0000 Subject: [PATCH 5/7] Embed the package icon and drop the stale nuspec PackageIconUrl is deprecated (NU5048) and pointed at an externally hosted image. The package now embeds icon.png, a PNG export of the 64x64 frame of the project's icon, through PackageIcon. ApplicationIcon and the empty Win32Resource only apply to executables, so they are removed from the class library together with the .ico file they used. IPData.nuspec was never used by dotnet pack (the SDK-style project generates its own manifest) and had drifted from the project file, so it is removed. Co-Authored-By: Claude Fable 5.1 --- src/IPData/IPData.csproj | 8 +++++--- src/IPData/IPData.nuspec | 23 ----------------------- src/IPData/icon.png | Bin 0 -> 969 bytes src/IPData/ipdata.ico | Bin 34494 -> 0 bytes 4 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 src/IPData/IPData.nuspec create mode 100644 src/IPData/icon.png delete mode 100644 src/IPData/ipdata.ico diff --git a/src/IPData/IPData.csproj b/src/IPData/IPData.csproj index 9a11a02..93153fd 100644 --- a/src/IPData/IPData.csproj +++ b/src/IPData/IPData.csproj @@ -12,10 +12,8 @@ git false MIT - https://image.ibb.co/iDQdUS/ipdatalogo.png - ipdata.ico + icon.png IPData Community - Release notes are at https://github.com/ipdata/dotnet/releases false false @@ -23,6 +21,10 @@ 3.1.0 + + + + diff --git a/src/IPData/IPData.nuspec b/src/IPData/IPData.nuspec deleted file mode 100644 index 4c76c2d..0000000 --- a/src/IPData/IPData.nuspec +++ /dev/null @@ -1,23 +0,0 @@ - - - - IPData - $version$ - IPData Contributors - IPData Contributors - false - https://licenses.nuget.org/MIT - https://github.com/ipdata/dotnet - https://image.ibb.co/iDQdUS/ipdatalogo.png - C# library to gather information for an IP using ipdata.co - Release notes are at https://github.com/ipdata/dotnet/releases - IPData Community - ip ipdata csharp - - - - - - - - \ No newline at end of file diff --git a/src/IPData/icon.png b/src/IPData/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fa19132622519f0743e626bd0f088ab5a283f4aa GIT binary patch literal 969 zcmV;)12+7LP)HL0h&|@oD9qd zRs+j5`V@_3U>VSFYxK`*j?7QsI*|8|_7?z`fLWz z2nFaiDL|XR@d`@0If#YqK1dz7dXi|VWf#V$(mO(PGU-+L|lLAx=98Xf` z1}1Sr&avPh_R*jB) z0P;2c%|*ZBwAqFL5fzR~JJSc81SSal98?jdZjHbi_TjIBQ}UN(kpñR;VI;zs= ztARB>g=_`hst7Y#iIQz#fU1G@SVq3e_gyQB^v8MzU;?Kc6rh-ZamOL@cL@P$K#K$9 z+ccs?W7vZ1xZH+qCuCd=k^D6rK$2#->epW56-}JLDs^5L6!`(Xa=KGE7oY&=aS(|r z2e(cBsUWWi91AsacZv1lUCnbEN5@jMr;B?Q$Uh*CkO6!&Nd6NJT#J>Wz@PxTiBpss zAYVotBL(^OmS}6n@Ku*EuvSHGI-ksJWQ^c~h5(ltLzoCW6OgaNLSZ9pp!ae=)f2vEd0LwOYW3dSL3;C*%0 rf_KCo3c@^D`3f9T@B1$PBOLqz2#m60UU5tF00000NkvXXu0mjfJ;SG+ literal 0 HcmV?d00001 diff --git a/src/IPData/ipdata.ico b/src/IPData/ipdata.ico deleted file mode 100644 index 604c685b099ccb4aed4aa5c000467cba29cfebd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34494 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 Date: Tue, 3 Nov 2026 12:55:01 +0000 Subject: [PATCH 6/7] Remove missing files from the solution items IPData.sln listed .github/CODE_OF_CONDUCT.md and .github/PULL_REQUEST_TEMPLATE.md as solution items, but neither file exists in the repository. Co-Authored-By: Claude Fable 5.1 --- IPData.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/IPData.sln b/IPData.sln index 6c5dbed..89994e9 100644 --- a/IPData.sln +++ b/IPData.sln @@ -17,9 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IPData.Tests", "test\Unit\I EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{3B7FCB46-B3B6-41EF-9495-3FA4D027E3FC}" ProjectSection(SolutionItems) = preProject - .github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md .github\CONTRIBUTING.md = .github\CONTRIBUTING.md - .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md .github\README.md = .github\README.md EndProjectSection EndProject From 02a852ef7a4c80cc8c1daf3a9a633bb40b9c7852 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:00:02 +0000 Subject: [PATCH 7/7] Update the test dependencies Move the test project to current releases: xunit 2.9.3 with xunit.runner.visualstudio 4.0.1 and Microsoft.NET.Test.Sdk 18.11.0, FluentAssertions 7.2.2 (the last line under the Apache-2.0 license) with FluentAssertions.Analyzers 0.33.0 (the newest release built against a Roslyn version that every .NET 8 SDK ships; 0.34 needs Roslyn 4.11), coverlet.msbuild 10.1.0, AutoFixture 4.18.1 and an explicit Moq 4.20.72 reference, which replaces the Moq 4.7.0 that AutoFixture.AutoMoq pulled in. The xunit analyzers flag ConfigureAwait(false) in test methods (xUnit1030) because it can bypass the runner's parallelization limits, so those calls are removed from the tests. All 137 tests pass on the new stack. Co-Authored-By: Claude Fable 5.1 --- .../Http/HttpClientAdapterTests.cs | 4 +- test/Unit/IPData.Tests/IPData.Tests.csproj | 17 ++--- test/Unit/IPData.Tests/IPDataClientTests.cs | 66 +++++++++---------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/test/Unit/IPData.Tests/Http/HttpClientAdapterTests.cs b/test/Unit/IPData.Tests/Http/HttpClientAdapterTests.cs index 32b5af0..488e5d6 100644 --- a/test/Unit/IPData.Tests/Http/HttpClientAdapterTests.cs +++ b/test/Unit/IPData.Tests/Http/HttpClientAdapterTests.cs @@ -71,8 +71,8 @@ public async Task SendAsync_WhenCalledRepeatedly_ShouldSendEveryRequestThroughTh var sut = new HttpClientAdapter(httpClient); // Act - using (var first = await sut.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.ipdata.co/8.8.8.8")).ConfigureAwait(false)) - using (var second = await sut.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.ipdata.co/1.1.1.1")).ConfigureAwait(false)) + using (var first = await sut.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.ipdata.co/8.8.8.8"))) + using (var second = await sut.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://api.ipdata.co/1.1.1.1"))) { // Assert first.StatusCode.Should().Be(HttpStatusCode.OK); diff --git a/test/Unit/IPData.Tests/IPData.Tests.csproj b/test/Unit/IPData.Tests/IPData.Tests.csproj index 627a296..7d38f61 100644 --- a/test/Unit/IPData.Tests/IPData.Tests.csproj +++ b/test/Unit/IPData.Tests/IPData.Tests.csproj @@ -15,18 +15,19 @@ - - - + + + all runtime; build; native; contentfiles; analyzers - - - + + + + - - + + all runtime; build; native; contentfiles; analyzers diff --git a/test/Unit/IPData.Tests/IPDataClientTests.cs b/test/Unit/IPData.Tests/IPDataClientTests.cs index eab03dc..602af12 100644 --- a/test/Unit/IPData.Tests/IPDataClientTests.cs +++ b/test/Unit/IPData.Tests/IPDataClientTests.cs @@ -125,7 +125,7 @@ public async Task Lookup_WhenCalled_ShouldSendUserAgentAndAcceptHeaders() var sut = new IPDataClient("test-api-key", httpClient.Object); // Act - await sut.Lookup("8.8.8.8").ConfigureAwait(false); + await sut.Lookup("8.8.8.8"); // Assert var request = requests.Should().ContainSingle().Which; @@ -147,7 +147,7 @@ public async Task Lookup_WhenCalledWithIpList_ShouldSendUserAgentAndAcceptHeader var sut = new IPDataClient("test-api-key", httpClient.Object); // Act - await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }).ConfigureAwait(false); + await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }); // Assert var request = requests.Should().ContainSingle().Which; @@ -195,7 +195,7 @@ public async Task Lookup_WhenCreatedWithHttpClientAndBaseUrl_ShouldSendThroughTh var sut = new IPDataClient("test-api-key", httpClient, new Uri("https://eu-api.ipdata.co")); // Act - var actual = await sut.Lookup("8.8.8.8").ConfigureAwait(false); + var actual = await sut.Lookup("8.8.8.8"); // Assert actual.Ip.Should().Be("8.8.8.8"); @@ -218,7 +218,7 @@ public async Task Lookup_WhenCalled_ShouldRequestHttp2WithFallbackToLowerVersion var sut = new IPDataClient("test-api-key", httpClient.Object); // Act - var actual = await sut.Lookup("8.8.8.8").ConfigureAwait(false); + var actual = await sut.Lookup("8.8.8.8"); // Assert actual.Ip.Should().Be("8.8.8.8"); @@ -248,7 +248,7 @@ public async Task Lookup_WhenCalledWithIpList_ShouldPostJsonAndRequestHttp2WithF var sut = new IPDataClient("test-api-key", httpClient.Object); // Act - var actual = await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }).ConfigureAwait(false); + var actual = await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }); // Assert actual.Select(x => x.Ip).Should().Equal("1.1.1.1", "2.2.2.2"); @@ -274,7 +274,7 @@ public async Task Lookup_WhenSuccessful_ShouldDisposeRequestAndResponseAfterRead var sut = new IPDataClient("test-api-key", httpClient.Object); // Act - var actual = await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }).ConfigureAwait(false); + var actual = await sut.Lookup(new[] { "1.1.1.1", "2.2.2.2" }); // Assert: the body was deserialized before anything was disposed... actual.Select(x => x.Ip).Should().Equal("1.1.1.1", "2.2.2.2"); @@ -282,8 +282,8 @@ public async Task Lookup_WhenSuccessful_ShouldDisposeRequestAndResponseAfterRead // ...and both messages are disposed afterwards, so their content can no longer be read. Func readRequest = () => request.Content.ReadAsStringAsync(); Func readResponse = () => response.Content.ReadAsStringAsync(); - await readRequest.Should().ThrowAsync().ConfigureAwait(false); - await readResponse.Should().ThrowAsync().ConfigureAwait(false); + await readRequest.Should().ThrowAsync(); + await readResponse.Should().ThrowAsync(); } [Fact] @@ -296,9 +296,9 @@ public async Task Lookup_WhenCalledRepeatedlyOnOneInstance_ShouldSendEveryReques var sut = new IPDataClient("test-api-key", httpClient); // Act - await sut.Lookup().ConfigureAwait(false); - await sut.Lookup("8.8.8.8").ConfigureAwait(false); - await sut.Threat("8.8.8.8").ConfigureAwait(false); + await sut.Lookup(); + await sut.Lookup("8.8.8.8"); + await sut.Threat("8.8.8.8"); // Assert var expectedUserAgent = ExpectedUserAgent; @@ -330,10 +330,10 @@ public async Task Lookup_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Lookup().ConfigureAwait(false); }; + Func act = async () => { await sut.Lookup(); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -349,10 +349,10 @@ public async Task Lookup_WhenCalledWithIpAndErrorStatusCode_ShouldThrowExpectedE .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Lookup("69.78.70.144").ConfigureAwait(false); }; + Func act = async () => { await sut.Lookup("69.78.70.144"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -369,10 +369,10 @@ public async Task Lookup_WhenCalledWithIpListAndErrorStatusCode_ShouldThrowExpec var sut = new IPDataClient("test-api-key", httpClient.Object); var ipList = new string[] { "1.1.1.1", "2.2.2.2", "3.3.3.3" }; - Func act = async () => { await sut.Lookup(ipList).ConfigureAwait(false); }; + Func act = async () => { await sut.Lookup(ipList); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -388,10 +388,10 @@ public async Task Lookup_WhenCalledWithSelectorAndErrorStatusCode_ShouldThrowExp .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Lookup("1.1.1.1", x => x.CountryName).ConfigureAwait(false); }; + Func act = async () => { await sut.Lookup("1.1.1.1", x => x.CountryName); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -407,10 +407,10 @@ public async Task Lookup_WhenCalledWithSelectorsAndErrorStatusCode_ShouldThrowEx .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Lookup("1.1.1.1", x => x.Asn, x => x.City).ConfigureAwait(false); }; + Func act = async () => { await sut.Lookup("1.1.1.1", x => x.Asn, x => x.City); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -426,10 +426,10 @@ public async Task Company_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Company("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.Company("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -445,10 +445,10 @@ public async Task Carrier_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Carrier("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.Carrier("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -464,10 +464,10 @@ public async Task Asn_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Asn("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.Asn("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -483,10 +483,10 @@ public async Task TimeZone_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.TimeZone("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.TimeZone("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -502,10 +502,10 @@ public async Task Currency_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Currency("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.Currency("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } @@ -521,10 +521,10 @@ public async Task Threat_WhenErrorStatusCode_ShouldThrowExpectedException( .ReturnsAsync(new HttpResponseMessage(statusCode)); var sut = new IPDataClient("test-api-key", httpClient.Object); - Func act = async () => { await sut.Threat("1.1.1.1").ConfigureAwait(false); }; + Func act = async () => { await sut.Threat("1.1.1.1"); }; // Act/Assert - (await act.Should().ThrowAsync().ConfigureAwait(false)) + (await act.Should().ThrowAsync()) .And.Should().BeOfType(expectedExceptionType); } }