From f1c5dae4273b5b6bf72a4180fcb1bf4cf2d1660e Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 3 Mar 2026 08:58:14 -0800 Subject: [PATCH 01/15] Convert all model and response classes to C# records Co-Authored-By: Claude Opus 4.6 --- .../DeserializationTests.cs | 4 - .../Model/LocationTests.cs | 7 +- MaxMind.GeoIP2.UnitTests/NamedEntityTests.cs | 24 +- MaxMind.GeoIP2.UnitTests/ResponseTests.cs | 8 +- .../WebServiceClientTests.cs | 1 - MaxMind.GeoIP2/CompatibilitySuppressions.xml | 10 +- MaxMind.GeoIP2/DatabaseReader.cs | 8 +- MaxMind.GeoIP2/MaxMind.GeoIP2.csproj | 6 +- MaxMind.GeoIP2/Model/Anonymizer.cs | 82 +---- MaxMind.GeoIP2/Model/City.cs | 32 +- MaxMind.GeoIP2/Model/Continent.cs | 33 +- MaxMind.GeoIP2/Model/Country.cs | 41 +-- MaxMind.GeoIP2/Model/Location.cs | 74 +---- MaxMind.GeoIP2/Model/MaxMind.cs | 36 +-- MaxMind.GeoIP2/Model/NamedEntity.cs | 59 +--- MaxMind.GeoIP2/Model/Postal.cs | 42 +-- MaxMind.GeoIP2/Model/RepresentedCountry.cs | 38 +-- MaxMind.GeoIP2/Model/Subdivision.cs | 36 +-- MaxMind.GeoIP2/Model/Traits.cs | 301 +++--------------- MaxMind.GeoIP2/Properties/AssemblyInfo.cs | 6 + .../Responses/AbstractCityResponse.cs | 100 ++---- .../Responses/AbstractCountryResponse.cs | 96 ++---- MaxMind.GeoIP2/Responses/AbstractResponse.cs | 18 +- .../Responses/AnonymousIPResponse.cs | 72 ++--- .../Responses/AnonymousPlusResponse.cs | 143 ++------- MaxMind.GeoIP2/Responses/AsnResponse.cs | 34 +- MaxMind.GeoIP2/Responses/CityResponse.cs | 39 +-- .../Responses/ConnectionTypeResponse.cs | 41 +-- MaxMind.GeoIP2/Responses/CountryResponse.cs | 32 +- MaxMind.GeoIP2/Responses/DomainResponse.cs | 35 +- .../Responses/EnterpriseResponse.cs | 28 +- MaxMind.GeoIP2/Responses/InsightsResponse.cs | 72 +---- MaxMind.GeoIP2/Responses/IspResponse.cs | 52 ++- MaxMind.GeoIP2/WebServiceClient.cs | 3 +- releasenotes.md | 26 ++ 35 files changed, 334 insertions(+), 1305 deletions(-) diff --git a/MaxMind.GeoIP2.UnitTests/DeserializationTests.cs b/MaxMind.GeoIP2.UnitTests/DeserializationTests.cs index 51c6e689..6781804c 100644 --- a/MaxMind.GeoIP2.UnitTests/DeserializationTests.cs +++ b/MaxMind.GeoIP2.UnitTests/DeserializationTests.cs @@ -1,13 +1,9 @@ -#region - using MaxMind.GeoIP2.Responses; using System.Text; using System.Text.Json; using Xunit; using static MaxMind.GeoIP2.UnitTests.ResponseHelper; -#endregion - namespace MaxMind.GeoIP2.UnitTests { public class DeserializationTests diff --git a/MaxMind.GeoIP2.UnitTests/Model/LocationTests.cs b/MaxMind.GeoIP2.UnitTests/Model/LocationTests.cs index 5a54182e..b4371dec 100644 --- a/MaxMind.GeoIP2.UnitTests/Model/LocationTests.cs +++ b/MaxMind.GeoIP2.UnitTests/Model/LocationTests.cs @@ -16,7 +16,10 @@ public class LocationTests public void HasCoordinatesFailure(double? latitude, double? longitude) { var location = new Location - (latitude: latitude, longitude: longitude); + { + Latitude = latitude, + Longitude = longitude + }; Assert.False(location.HasCoordinates); } @@ -24,7 +27,7 @@ public void HasCoordinatesFailure(double? latitude, double? longitude) [Fact] public void HasCoordinatesSuccess() { - var location = new Location(latitude: 50.0, longitude: 0.0); + var location = new Location { Latitude = 50.0, Longitude = 0.0 }; Assert.True(location.HasCoordinates); } } diff --git a/MaxMind.GeoIP2.UnitTests/NamedEntityTests.cs b/MaxMind.GeoIP2.UnitTests/NamedEntityTests.cs index bb9567c2..5d12e9c2 100644 --- a/MaxMind.GeoIP2.UnitTests/NamedEntityTests.cs +++ b/MaxMind.GeoIP2.UnitTests/NamedEntityTests.cs @@ -1,11 +1,7 @@ -#region - using MaxMind.GeoIP2.Model; using System.Collections.Generic; using Xunit; -#endregion - namespace MaxMind.GeoIP2.UnitTests { public class NamedEntityTests @@ -13,10 +9,11 @@ public class NamedEntityTests [Fact] public void CanGetSingleName() { - var c = new City( - names: new Dictionary { { "en", "Foo" } }, - locales: new List { "en" } - ); + var c = new City + { + Names = new Dictionary { { "en", "Foo" } }, + Locales = new List { "en" } + }; Assert.Equal("Foo", c.Name); } @@ -24,12 +21,13 @@ public void CanGetSingleName() [Fact] public void NameReturnsCorrectLocale() { - var c = new City( - names: new Dictionary { { "en", "Mexico City" }, { "es", "Ciudad de México" } }, - locales: new List { "es" } - ); + var c = new City + { + Names = new Dictionary { { "en", "Mexico City" }, { "es", "Ciudad de México" } }, + Locales = new List { "es" } + }; Assert.Equal("Ciudad de México", c.Name); } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2.UnitTests/ResponseTests.cs b/MaxMind.GeoIP2.UnitTests/ResponseTests.cs index 89345e59..12971922 100644 --- a/MaxMind.GeoIP2.UnitTests/ResponseTests.cs +++ b/MaxMind.GeoIP2.UnitTests/ResponseTests.cs @@ -1,11 +1,7 @@ -#region - using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; using Xunit; -#endregion - namespace MaxMind.GeoIP2.UnitTests { public class ResponseTests @@ -14,9 +10,9 @@ public class ResponseTests public void InsightsConstruction() { var city = new City(); - var insightsReponse = new InsightsResponse(city: city); + var insightsReponse = new InsightsResponse { City = city }; Assert.Equal(insightsReponse.City, city); } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs b/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs index acce716e..72c25911 100644 --- a/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs +++ b/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs @@ -449,7 +449,6 @@ public void MissingKeys() Assert.Null(r.GeoNameId); Assert.Null(r.Name); Assert.Empty(r.Names); - Assert.Equal("", r.ToString()); } } diff --git a/MaxMind.GeoIP2/CompatibilitySuppressions.xml b/MaxMind.GeoIP2/CompatibilitySuppressions.xml index cd94d4f1..6cd0fce5 100644 --- a/MaxMind.GeoIP2/CompatibilitySuppressions.xml +++ b/MaxMind.GeoIP2/CompatibilitySuppressions.xml @@ -1,15 +1,17 @@  + - CP0002 - M:MaxMind.GeoIP2.Model.Anonymizer.#ctor(System.Nullable{System.Int32},System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String) + CP0005 + M:MaxMind.GeoIP2.Responses.AbstractCityResponse.{Clone}$ lib/netstandard2.1/MaxMind.GeoIP2.dll lib/net8.0/MaxMind.GeoIP2.dll - CP0002 - M:MaxMind.GeoIP2.Responses.AnonymousPlusResponse.#ctor(System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,MaxMind.Db.Network,System.String) + CP0005 + M:MaxMind.GeoIP2.Responses.AbstractCountryResponse.{Clone}$ lib/netstandard2.1/MaxMind.GeoIP2.dll lib/net8.0/MaxMind.GeoIP2.dll diff --git a/MaxMind.GeoIP2/DatabaseReader.cs b/MaxMind.GeoIP2/DatabaseReader.cs index bce90d2b..2f8e6930 100644 --- a/MaxMind.GeoIP2/DatabaseReader.cs +++ b/MaxMind.GeoIP2/DatabaseReader.cs @@ -525,15 +525,9 @@ private bool TryExecute(IPAddress ipAddress, string type, [MaybeNullWhen(fals var injectables = new InjectableValues(); injectables.AddValue("ip_address", ipStr); + injectables.AddValue("locales", _locales); var response = _reader.Find(ipAddress, injectables); - if (response == null) - { - return null; - } - - response.SetLocales(_locales); - return response; } } diff --git a/MaxMind.GeoIP2/MaxMind.GeoIP2.csproj b/MaxMind.GeoIP2/MaxMind.GeoIP2.csproj index eba13283..71b2973e 100644 --- a/MaxMind.GeoIP2/MaxMind.GeoIP2.csproj +++ b/MaxMind.GeoIP2/MaxMind.GeoIP2.csproj @@ -44,7 +44,7 @@ - + @@ -52,6 +52,10 @@ 10.0.3 + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/MaxMind.GeoIP2/Model/Anonymizer.cs b/MaxMind.GeoIP2/Model/Anonymizer.cs index 19eb2b50..ec057582 100644 --- a/MaxMind.GeoIP2/Model/Anonymizer.cs +++ b/MaxMind.GeoIP2/Model/Anonymizer.cs @@ -1,55 +1,14 @@ -#region - using System; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains anonymizer-related data associated with an IP address. /// This data is available from the GeoIP2 Insights web service. /// - public class Anonymizer + public record Anonymizer { - /// - /// Constructor - /// - public Anonymizer() - { - } - - /// - /// Constructor - /// - public Anonymizer( - int? confidence = null, - bool isAnonymous = false, - bool isAnonymousVpn = false, - bool isHostingProvider = false, - bool isPublicProxy = false, - bool isResidentialProxy = false, - bool isTorExitNode = false, -#if NET6_0_OR_GREATER - DateOnly? networkLastSeen = null, -#endif - string? providerName = null - ) - { - Confidence = confidence; - IsAnonymous = isAnonymous; - IsAnonymousVpn = isAnonymousVpn; - IsHostingProvider = isHostingProvider; - IsPublicProxy = isPublicProxy; - IsResidentialProxy = isResidentialProxy; - IsTorExitNode = isTorExitNode; -#if NET6_0_OR_GREATER - NetworkLastSeen = networkLastSeen; -#endif - ProviderName = providerName; - } - /// /// A score ranging from 1 to 99 that represents our percent confidence /// that the network is currently part of an actively used VPN service. @@ -57,7 +16,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("confidence")] - public int? Confidence { get; internal set; } + public int? Confidence { get; init; } /// /// This is true if the IP address belongs to any sort of anonymous @@ -65,7 +24,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_anonymous")] - public bool IsAnonymous { get; internal set; } + public bool IsAnonymous { get; init; } /// /// This is true if the IP address is registered to an anonymous @@ -79,7 +38,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_anonymous_vpn")] - public bool IsAnonymousVpn { get; internal set; } + public bool IsAnonymousVpn { get; init; } /// /// This is true if the IP address belongs to a hosting or VPN @@ -88,7 +47,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_hosting_provider")] - public bool IsHostingProvider { get; internal set; } + public bool IsHostingProvider { get; init; } /// /// This is true if the IP address belongs to a public proxy. @@ -96,7 +55,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_public_proxy")] - public bool IsPublicProxy { get; internal set; } + public bool IsPublicProxy { get; init; } /// /// This is true if the IP address is on a suspected anonymizing @@ -105,7 +64,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_residential_proxy")] - public bool IsResidentialProxy { get; internal set; } + public bool IsResidentialProxy { get; init; } /// /// This is true if the IP address belongs to a Tor exit node. @@ -113,7 +72,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("is_tor_exit_node")] - public bool IsTorExitNode { get; internal set; } + public bool IsTorExitNode { get; init; } #if NET6_0_OR_GREATER /// @@ -123,7 +82,7 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("network_last_seen")] - public DateOnly? NetworkLastSeen { get; internal set; } + public DateOnly? NetworkLastSeen { get; init; } #endif /// @@ -133,27 +92,6 @@ public Anonymizer( /// [JsonInclude] [JsonPropertyName("provider_name")] - public string? ProviderName { get; internal set; } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"{nameof(Confidence)}: {Confidence}, " + - $"{nameof(IsAnonymous)}: {IsAnonymous}, " + - $"{nameof(IsAnonymousVpn)}: {IsAnonymousVpn}, " + - $"{nameof(IsHostingProvider)}: {IsHostingProvider}, " + - $"{nameof(IsPublicProxy)}: {IsPublicProxy}, " + - $"{nameof(IsResidentialProxy)}: {IsResidentialProxy}, " + - $"{nameof(IsTorExitNode)}: {IsTorExitNode}, " + -#if NET6_0_OR_GREATER - $"{nameof(NetworkLastSeen)}: {NetworkLastSeen}, " + -#endif - $"{nameof(ProviderName)}: {ProviderName}"; - } + public string? ProviderName { get; init; } } } diff --git a/MaxMind.GeoIP2/Model/City.cs b/MaxMind.GeoIP2/Model/City.cs index 2e3eb602..c9140aae 100644 --- a/MaxMind.GeoIP2/Model/City.cs +++ b/MaxMind.GeoIP2/Model/City.cs @@ -1,11 +1,6 @@ -#region - using MaxMind.Db; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// @@ -15,28 +10,8 @@ namespace MaxMind.GeoIP2.Model /// Do not use any of the city names as a database or dictionary /// key. Use the instead. /// - public class City : NamedEntity + public record City : NamedEntity { - /// - /// Constructor - /// - public City() - { - } - - /// - /// Constructor - /// - [Constructor] - public City(int? confidence = null, - [Parameter("geoname_id")] long? geoNameId = null, - IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - : base(geoNameId, names, locales) - { - Confidence = confidence; - } - /// /// A value from 0-100 indicating MaxMind's confidence that the city /// is correct. This value is only set when using the Insights @@ -44,6 +19,7 @@ public City(int? confidence = null, /// [JsonInclude] [JsonPropertyName("confidence")] - public int? Confidence { get; internal set; } + [MapKey("confidence")] + public int? Confidence { get; init; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Model/Continent.cs b/MaxMind.GeoIP2/Model/Continent.cs index 36dcc51f..7898094b 100644 --- a/MaxMind.GeoIP2/Model/Continent.cs +++ b/MaxMind.GeoIP2/Model/Continent.cs @@ -1,11 +1,6 @@ -#region - using MaxMind.Db; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// @@ -14,35 +9,15 @@ namespace MaxMind.GeoIP2.Model /// key. Use the or /// instead. /// - public class Continent : NamedEntity + public record Continent : NamedEntity { - /// - /// Constructor - /// - public Continent() - { - } - - /// - /// Constructor - /// - [Constructor] - public Continent( - string? code = null, - [Parameter("geoname_id")] long? geoNameId = null, - IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - : base(geoNameId, names, locales) - { - Code = code; - } - /// /// A two character continent code like "NA" (North America) or "OC" /// (Oceania). /// [JsonInclude] [JsonPropertyName("code")] - public string? Code { get; internal set; } + [MapKey("code")] + public string? Code { get; init; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Model/Country.cs b/MaxMind.GeoIP2/Model/Country.cs index c8998a2d..ca3df43f 100644 --- a/MaxMind.GeoIP2/Model/Country.cs +++ b/MaxMind.GeoIP2/Model/Country.cs @@ -1,11 +1,6 @@ -#region - using MaxMind.Db; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// @@ -14,33 +9,8 @@ namespace MaxMind.GeoIP2.Model /// key. Use the or /// instead. /// - public class Country : NamedEntity + public record Country : NamedEntity { - /// - /// Constructor - /// - public Country() - { - } - - /// - /// Constructor - /// - [Constructor] - public Country( - int? confidence = null, - [Parameter("geoname_id")] long? geoNameId = null, - [Parameter("is_in_european_union")] bool isInEuropeanUnion = false, - [Parameter("iso_code")] string? isoCode = null, - IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - : base(geoNameId, names, locales) - { - Confidence = confidence; - IsoCode = isoCode; - IsInEuropeanUnion = isInEuropeanUnion; - } - /// /// A value from 0-100 indicating MaxMind's confidence that the country /// is correct. This value is only set when using the Insights @@ -48,7 +18,8 @@ public Country( /// [JsonInclude] [JsonPropertyName("confidence")] - public int? Confidence { get; internal set; } + [MapKey("confidence")] + public int? Confidence { get; init; } /// /// This is true if the country is a member state of the @@ -57,7 +28,8 @@ public Country( /// [JsonInclude] [JsonPropertyName("is_in_european_union")] - public bool IsInEuropeanUnion { get; internal set; } + [MapKey("is_in_european_union")] + public bool IsInEuropeanUnion { get; init; } /// /// The @@ -70,6 +42,7 @@ public Country( /// [JsonInclude] [JsonPropertyName("iso_code")] - public string? IsoCode { get; internal set; } + [MapKey("iso_code")] + public string? IsoCode { get; init; } } } diff --git a/MaxMind.GeoIP2/Model/Location.cs b/MaxMind.GeoIP2/Model/Location.cs index b53a8f7c..b1f3e688 100644 --- a/MaxMind.GeoIP2/Model/Location.cs +++ b/MaxMind.GeoIP2/Model/Location.cs @@ -1,45 +1,14 @@ -#region - -using MaxMind.Db; using System; +using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains data for the location record associated with an IP address. /// - public class Location + public record Location { - /// - /// Constructor - /// - public Location() - { - } - - /// - /// Constructor - /// - [Constructor] - public Location( - [Parameter("accuracy_radius")] int? accuracyRadius = null, - double? latitude = null, - double? longitude = null, - [Parameter("metro_code")] int? metroCode = null, - [Parameter("time_zone")] string? timeZone = null) - { - AccuracyRadius = accuracyRadius; - Latitude = latitude; - Longitude = longitude; -#pragma warning disable 618 - MetroCode = metroCode; -#pragma warning restore 618 - TimeZone = timeZone; - } - /// /// The approximate accuracy radius in kilometers around the /// latitude and longitude for the IP address. This is the radius @@ -49,14 +18,16 @@ public Location( /// [JsonInclude] [JsonPropertyName("accuracy_radius")] - public int? AccuracyRadius { get; internal set; } + [MapKey("accuracy_radius")] + public int? AccuracyRadius { get; init; } /// /// The average income in US dollars associated with the IP address. /// [JsonInclude] [JsonPropertyName("average_income")] - public int? AverageIncome { get; internal set; } + [MapKey("average_income")] + public int? AverageIncome { get; init; } /// /// Determines whether both the Latitude @@ -72,7 +43,8 @@ public Location( /// [JsonInclude] [JsonPropertyName("latitude")] - public double? Latitude { get; internal set; } + [MapKey("latitude")] + public double? Latitude { get; init; } /// /// The approximate longitude of the location associated with the @@ -81,7 +53,8 @@ public Location( /// [JsonInclude] [JsonPropertyName("longitude")] - public double? Longitude { get; internal set; } + [MapKey("longitude")] + public double? Longitude { get; init; } /// /// The metro code is a no-longer-maintained code for targeting @@ -89,15 +62,17 @@ public Location( /// [JsonInclude] [JsonPropertyName("metro_code")] + [MapKey("metro_code")] [Obsolete("Code values are no longer maintained.")] - public int? MetroCode { get; internal set; } + public int? MetroCode { get; init; } /// /// The estimated number of people per square kilometer. /// [JsonInclude] [JsonPropertyName("population_density")] - public int? PopulationDensity { get; internal set; } + [MapKey("population_density")] + public int? PopulationDensity { get; init; } /// /// The time zone associated with location, as specified by the @@ -110,24 +85,7 @@ public Location( /// [JsonInclude] [JsonPropertyName("time_zone")] - public string? TimeZone { get; internal set; } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return "Location [ " - + (AccuracyRadius.HasValue ? "AccuracyRadius=" + AccuracyRadius + ", " : string.Empty) - + (Latitude.HasValue ? "Latitude=" + Latitude + ", " : string.Empty) - + (Longitude.HasValue ? "Longitude=" + Longitude + ", " : string.Empty) -#pragma warning disable 618 - + (MetroCode.HasValue ? "MetroCode=" + MetroCode + ", " : string.Empty) -#pragma warning restore 618 - + (TimeZone != null ? "TimeZone=" + TimeZone : "") + "]"; - } + [MapKey("time_zone")] + public string? TimeZone { get; init; } } } diff --git a/MaxMind.GeoIP2/Model/MaxMind.cs b/MaxMind.GeoIP2/Model/MaxMind.cs index a01c7c20..55a827bf 100644 --- a/MaxMind.GeoIP2/Model/MaxMind.cs +++ b/MaxMind.GeoIP2/Model/MaxMind.cs @@ -1,33 +1,12 @@ -#region - -using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains data related to your MaxMind account. /// - public class MaxMind + public record MaxMind { - /// - /// Constructor - /// - public MaxMind() - { - } - - /// - /// Constructor - /// - [Constructor] - public MaxMind([Parameter("queries_remaining")] int queriesRemaining) - { - QueriesRemaining = queriesRemaining; - } - /// /// The number of remaining queries in your account for the web /// service end point. This will be null when using a local @@ -35,17 +14,6 @@ public MaxMind([Parameter("queries_remaining")] int queriesRemaining) /// [JsonInclude] [JsonPropertyName("queries_remaining")] - public int? QueriesRemaining { get; internal set; } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"MaxMind [ QueriesRemaining={QueriesRemaining} ]"; - } + public int? QueriesRemaining { get; init; } } } diff --git a/MaxMind.GeoIP2/Model/NamedEntity.cs b/MaxMind.GeoIP2/Model/NamedEntity.cs index 39715f12..0f048990 100644 --- a/MaxMind.GeoIP2/Model/NamedEntity.cs +++ b/MaxMind.GeoIP2/Model/NamedEntity.cs @@ -1,32 +1,15 @@ -#region - using MaxMind.Db; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// - /// Abstract class for records with name maps. + /// Abstract record for entities with name maps. /// - public abstract class NamedEntity + public abstract record NamedEntity { - /// - /// Constructor - /// - [Constructor] - protected NamedEntity(long? geoNameId = null, IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - { - Names = names ?? new ReadOnlyDictionary(new Dictionary()); - GeoNameId = geoNameId; - Locales = locales ?? ["en"]; - } - /// /// A /// from locale codes to the name in that locale. Don't use any of @@ -37,48 +20,36 @@ protected NamedEntity(long? geoNameId = null, IReadOnlyDictionary [JsonInclude] [JsonPropertyName("names")] - public IReadOnlyDictionary Names { get; internal set; } + [MapKey("names")] + public IReadOnlyDictionary Names { get; init; } + = new Dictionary(); /// /// The GeoName ID for the city. /// [JsonInclude] [JsonPropertyName("geoname_id")] - public long? GeoNameId { get; internal set; } + [MapKey("geoname_id")] + public long? GeoNameId { get; init; } /// - /// Gets or sets the locales specified by the user. + /// The locales specified by the user. /// [JsonIgnore] - protected internal IReadOnlyList Locales { get; set; } + [Inject("locales")] + public IReadOnlyList Locales { get; init; } = ["en"]; /// - /// The name of the city based on the locales list passed to the - /// constructor. Don't use any of + /// The name based on the locales list passed to the + /// or DatabaseReader. Don't use any of /// these names as a database or dictionary key. Use the /// /// or relevant code instead. /// [JsonIgnore] - public string? Name - { - get - { - var locale = Locales.FirstOrDefault(l => Names.ContainsKey(l)); - return locale == null ? null : Names[locale]; - } - } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return Name ?? string.Empty; - } + public string? Name => + Locales.FirstOrDefault(l => Names.ContainsKey(l)) is { } locale + ? Names[locale] : null; } } diff --git a/MaxMind.GeoIP2/Model/Postal.cs b/MaxMind.GeoIP2/Model/Postal.cs index 284ce9b0..2f19ae4b 100644 --- a/MaxMind.GeoIP2/Model/Postal.cs +++ b/MaxMind.GeoIP2/Model/Postal.cs @@ -1,34 +1,13 @@ -#region - using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains data for the postal record associated with an IP address. /// - public class Postal + public record Postal { - /// - /// Constructor - /// - public Postal() - { - } - - /// - /// Constructor - /// - [Constructor] - public Postal(string? code = null, int? confidence = null) - { - Code = code; - Confidence = confidence; - } - /// /// The postal code of the location. Postal codes are not available /// for all countries. In some countries, this will only contain part @@ -36,7 +15,8 @@ public Postal(string? code = null, int? confidence = null) /// [JsonInclude] [JsonPropertyName("code")] - public string? Code { get; internal set; } + [MapKey("code")] + public string? Code { get; init; } /// /// A value from 0-100 indicating MaxMind's confidence that the @@ -45,17 +25,7 @@ public Postal(string? code = null, int? confidence = null) /// [JsonInclude] [JsonPropertyName("confidence")] - public int? Confidence { get; internal set; } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"Code: {Code}, Confidence: {Confidence}"; - } + [MapKey("confidence")] + public int? Confidence { get; init; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Model/RepresentedCountry.cs b/MaxMind.GeoIP2/Model/RepresentedCountry.cs index ac776494..af2bc0d4 100644 --- a/MaxMind.GeoIP2/Model/RepresentedCountry.cs +++ b/MaxMind.GeoIP2/Model/RepresentedCountry.cs @@ -1,48 +1,19 @@ -#region - using MaxMind.Db; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains data for the represented country associated with an IP address. - /// This class contains the country-level data associated with an IP address for + /// This record contains the country-level data associated with an IP address for /// the IP's represented country. The represented country is the country /// represented by something like a military base. /// Do not use any of the country names as a database or dictionary /// key. Use the or /// instead. /// - public class RepresentedCountry : Country + public record RepresentedCountry : Country { - /// - /// Constructor - /// - public RepresentedCountry() - { - } - - /// - /// Constructor - /// - [Constructor] - public RepresentedCountry( - string? type = null, - int? confidence = null, - [Parameter("geoname_id")] long? geoNameId = null, - [Parameter("is_in_european_union")] bool isInEuropeanUnion = false, - [Parameter("iso_code")] string? isoCode = null, - IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - : base(confidence, geoNameId, isInEuropeanUnion, isoCode, names, locales) - { - Type = type; - } - /// /// A string indicating the type of entity that is representing the /// country. Currently we only return military but this could @@ -50,6 +21,7 @@ public RepresentedCountry( /// [JsonInclude] [JsonPropertyName("type")] - public string? Type { get; internal set; } + [MapKey("type")] + public string? Type { get; init; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Model/Subdivision.cs b/MaxMind.GeoIP2/Model/Subdivision.cs index 665255ec..c0d5bcf5 100644 --- a/MaxMind.GeoIP2/Model/Subdivision.cs +++ b/MaxMind.GeoIP2/Model/Subdivision.cs @@ -1,11 +1,6 @@ -#region - using MaxMind.Db; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// @@ -14,31 +9,8 @@ namespace MaxMind.GeoIP2.Model /// key. Use the or /// instead. /// - public class Subdivision : NamedEntity + public record Subdivision : NamedEntity { - /// - /// Constructor - /// - public Subdivision() - { - } - - /// - /// Constructor - /// - [Constructor] - public Subdivision( - int? confidence = null, - [Parameter("geoname_id")] long? geoNameId = null, - [Parameter("iso_code")] string? isoCode = null, - IReadOnlyDictionary? names = null, - IReadOnlyList? locales = null) - : base(geoNameId, names, locales) - { - Confidence = confidence; - IsoCode = isoCode; - } - /// /// This is a value from 0-100 indicating MaxMind's confidence that /// the subdivision is correct. This value is only set when using the @@ -46,7 +18,8 @@ public Subdivision( /// [JsonInclude] [JsonPropertyName("confidence")] - public int? Confidence { get; internal set; } + [MapKey("confidence")] + public int? Confidence { get; init; } /// /// This is a string up to three characters long contain the @@ -59,6 +32,7 @@ public Subdivision( /// [JsonInclude] [JsonPropertyName("iso_code")] - public string? IsoCode { get; internal set; } + [MapKey("iso_code")] + public string? IsoCode { get; init; } } } diff --git a/MaxMind.GeoIP2/Model/Traits.cs b/MaxMind.GeoIP2/Model/Traits.cs index 52ae1c26..a78317c2 100644 --- a/MaxMind.GeoIP2/Model/Traits.cs +++ b/MaxMind.GeoIP2/Model/Traits.cs @@ -1,201 +1,14 @@ -#region - using MaxMind.Db; using System; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Model { /// /// Contains data for the traits record associated with an IP address. /// - public class Traits + public record Traits { - /// - /// Constructor - /// - public Traits() - { - } - - /// - /// Constructor - /// - [Constructor] - public Traits( - [Parameter("autonomous_system_number")] long? autonomousSystemNumber = null, - [Parameter("autonomous_system_organization")] string? autonomousSystemOrganization = null, - [Parameter("connection_type")] string? connectionType = null, - string? domain = null, - [Inject("ip_address")] string? ipAddress = null, - [Parameter("ip_risk_snapshot")] double? ipRiskSnapshot = null, - [Parameter("is_anonymous")] bool isAnonymous = false, - [Parameter("is_anonymous_proxy")] bool isAnonymousProxy = false, - [Parameter("is_anonymous_vpn")] bool isAnonymousVpn = false, - [Parameter("is_anycast")] bool isAnycast = false, - [Parameter("is_hosting_provider")] bool isHostingProvider = false, - [Parameter("is_legitimate_proxy")] bool isLegitimateProxy = false, - [Parameter("is_public_proxy")] bool isPublicProxy = false, - [Parameter("is_residential_proxy")] bool isResidentialProxy = false, - [Parameter("is_satellite_provider")] bool isSatelliteProvider = false, - [Parameter("is_tor_exit_node")] bool isTorExitNode = false, - string? isp = null, - [Parameter("mobile_country_code")] string? mobileCountryCode = null, - [Parameter("mobile_network_code")] string? mobileNetworkCode = null, - string? organization = null, - [Parameter("user_type")] string? userType = null, - [Network] Network? network = null, - [Parameter("static_ip_score")] double? staticIPScore = null, - [Parameter("user_count")] int? userCount = null - ) - { - AutonomousSystemNumber = autonomousSystemNumber; - AutonomousSystemOrganization = autonomousSystemOrganization; - ConnectionType = connectionType; - Domain = domain; - IPAddress = ipAddress; - IpRiskSnapshot = ipRiskSnapshot; -#pragma warning disable 618 - IsAnonymous = isAnonymous; - IsAnonymousProxy = isAnonymousProxy; - IsAnonymousVpn = isAnonymousVpn; -#pragma warning restore 618 - IsAnycast = isAnycast; -#pragma warning disable 618 - IsHostingProvider = isHostingProvider; -#pragma warning restore 618 - IsLegitimateProxy = isLegitimateProxy; -#pragma warning disable 618 - IsPublicProxy = isPublicProxy; - IsResidentialProxy = isResidentialProxy; - IsSatelliteProvider = isSatelliteProvider; - IsTorExitNode = isTorExitNode; -#pragma warning restore 618 - Isp = isp; - MobileCountryCode = mobileCountryCode; - MobileNetworkCode = mobileNetworkCode; - Network = network; - Organization = organization; - StaticIPScore = staticIPScore; - UserCount = userCount; - UserType = userType; - } - - /// - /// Constructor for binary compatibility. - /// - [Obsolete("Use constructor with ipRiskSnapshot parameter")] - public Traits( - long? autonomousSystemNumber, - string? autonomousSystemOrganization, - string? connectionType, - string? domain, - string? ipAddress, - bool isAnonymous, - bool isAnonymousProxy, - bool isAnonymousVpn, - bool isAnycast, - bool isHostingProvider, - bool isLegitimateProxy, - bool isPublicProxy, - bool isResidentialProxy, - bool isSatelliteProvider, - bool isTorExitNode, - string? isp, - string? mobileCountryCode, - string? mobileNetworkCode, - string? organization, - string? userType, - Network? network, - double? staticIPScore, - int? userCount - ) : this( - autonomousSystemNumber, - autonomousSystemOrganization, - connectionType, - domain, - ipAddress, - null, // ipRiskSnapshot - isAnonymous, - isAnonymousProxy, - isAnonymousVpn, - isAnycast, - isHostingProvider, - isLegitimateProxy, - isPublicProxy, - isResidentialProxy, - isSatelliteProvider, - isTorExitNode, - isp, - mobileCountryCode, - mobileNetworkCode, - organization, - userType, - network, - staticIPScore, - userCount - ) - { - } - - /// - /// Constructor for binary compatibility. - /// - [Obsolete] - public Traits( - long? autonomousSystemNumber, - string? autonomousSystemOrganization, - string? connectionType, - string? domain, - string? ipAddress, - bool isAnonymous, - bool isAnonymousProxy, - bool isAnonymousVpn, - bool isHostingProvider, - bool isLegitimateProxy, - bool isPublicProxy, - bool isResidentialProxy, - bool isSatelliteProvider, - bool isTorExitNode, - string? isp, - string? mobileCountryCode, - string? mobileNetworkCode, - string? organization, - string? userType, - Network? network, - double? staticIPScore, - int? userCount - ) : this( - autonomousSystemNumber, - autonomousSystemOrganization, - connectionType, - domain, - ipAddress, - null, // ipRiskSnapshot - isAnonymous, - isAnonymousProxy, - isAnonymousVpn, - false, // isAnycast - isHostingProvider, - isLegitimateProxy, - isPublicProxy, - isResidentialProxy, - isSatelliteProvider, - isTorExitNode, - isp, - mobileCountryCode, - mobileNetworkCode, - organization, - userType, - network, - staticIPScore, - userCount - ) - { - } - /// /// The /// [JsonInclude] [JsonPropertyName("autonomous_system_number")] - public long? AutonomousSystemNumber { get; internal set; } + [MapKey("autonomous_system_number")] + public long? AutonomousSystemNumber { get; init; } /// /// The organization associated with the registered @@ -221,7 +35,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("autonomous_system_organization")] - public string? AutonomousSystemOrganization { get; internal set; } + [MapKey("autonomous_system_organization")] + public string? AutonomousSystemOrganization { get; init; } /// /// The connection type may take the following values: "Dialup", @@ -232,7 +47,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("connection_type")] - public string? ConnectionType { get; internal set; } + [MapKey("connection_type")] + public string? ConnectionType { get; init; } /// /// The second level domain associated with the IP address. This will @@ -242,7 +58,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("domain")] - public string? Domain { get; internal set; } + [MapKey("domain")] + public string? Domain { get; init; } /// /// The IP address that the data in the model is for. If you @@ -253,7 +70,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// A risk score associated with the IP address, ranging from 0.01 to 99. @@ -273,7 +91,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("ip_risk_snapshot")] - public double? IpRiskSnapshot { get; internal set; } + [MapKey("ip_risk_snapshot")] + public double? IpRiskSnapshot { get; init; } /// /// This is true if the IP address belongs to any sort of anonymous @@ -283,7 +102,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_anonymous")] - public bool IsAnonymous { get; internal set; } + [MapKey("is_anonymous")] + public bool IsAnonymous { get; init; } /// /// This is true if the IP is an anonymous proxy. @@ -291,7 +111,8 @@ public Traits( [JsonInclude] [JsonPropertyName("is_anonymous_proxy")] [Obsolete("Use our GeoIP2 Anonymous IP database instead.")] - public bool IsAnonymousProxy { get; internal set; } + [MapKey("is_anonymous_proxy")] + public bool IsAnonymousProxy { get; init; } /// /// This is true if the IP address belongs to an [JsonInclude] [JsonPropertyName("is_anycast")] - public bool IsAnycast { get; internal set; } + [MapKey("is_anycast")] + public bool IsAnycast { get; init; } /// /// This is true if the IP address is registered to an anonymous @@ -316,7 +138,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_anonymous_vpn")] - public bool IsAnonymousVpn { get; internal set; } + [MapKey("is_anonymous_vpn")] + public bool IsAnonymousVpn { get; init; } /// /// This is true if the IP address belongs to a hosting or VPN @@ -327,7 +150,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_hosting_provider")] - public bool IsHostingProvider { get; internal set; } + [MapKey("is_hosting_provider")] + public bool IsHostingProvider { get; init; } /// /// True if MaxMind believes this IP address to be a legitimate @@ -336,7 +160,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("is_legitimate_proxy")] - public bool IsLegitimateProxy { get; internal set; } + [MapKey("is_legitimate_proxy")] + public bool IsLegitimateProxy { get; init; } /// /// This is true if the IP address belongs to a public proxy. @@ -346,7 +171,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_public_proxy")] - public bool IsPublicProxy { get; internal set; } + [MapKey("is_public_proxy")] + public bool IsPublicProxy { get; init; } /// /// This is true if the IP address is on a suspected anonymizing @@ -356,7 +182,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_residential_proxy")] - public bool IsResidentialProxy { get; internal set; } + [MapKey("is_residential_proxy")] + public bool IsResidentialProxy { get; init; } /// /// This is true if the IP belong to a satellite Internet provider. @@ -364,7 +191,8 @@ public Traits( [JsonInclude] [JsonPropertyName("is_satellite_provider")] [Obsolete("Due to increased mobile usage, we have insufficient data to maintain this field.")] - public bool IsSatelliteProvider { get; internal set; } + [MapKey("is_satellite_provider")] + public bool IsSatelliteProvider { get; init; } /// /// This is true if the IP address belongs to a Tor exit node. @@ -374,7 +202,8 @@ public Traits( [Obsolete("Please use the Anonymizer object on the response instead.")] [JsonInclude] [JsonPropertyName("is_tor_exit_node")] - public bool IsTorExitNode { get; internal set; } + [MapKey("is_tor_exit_node")] + public bool IsTorExitNode { get; init; } /// /// The name of the ISP associated with the IP address. This value @@ -383,7 +212,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("isp")] - public string? Isp { get; internal set; } + [MapKey("isp")] + public string? Isp { get; init; } /// /// The @@ -393,7 +223,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("mobile_country_code")] - public string? MobileCountryCode { get; internal set; } + [MapKey("mobile_country_code")] + public string? MobileCountryCode { get; init; } /// /// The @@ -403,7 +234,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("mobile_network_code")] - public string? MobileNetworkCode { get; internal set; } + [MapKey("mobile_network_code")] + public string? MobileNetworkCode { get; init; } /// /// The network associated with the record. In particular, this is @@ -412,7 +244,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } + [Network] + public Network? Network { get; init; } /// /// The name of the organization associated with the IP address. This @@ -421,7 +254,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("organization")] - public string? Organization { get; internal set; } + [MapKey("organization")] + public string? Organization { get; init; } /// /// An indicator of how static or dynamic an IP address is. The value @@ -436,7 +270,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("static_ip_score")] - public double? StaticIPScore { get; internal set; } + [MapKey("static_ip_score")] + public double? StaticIPScore { get; init; } /// /// The estimated number of users sharing the IP/network during the past @@ -446,7 +281,8 @@ public Traits( /// [JsonInclude] [JsonPropertyName("user_count")] - public int? UserCount { get; internal set; } + [MapKey("user_count")] + public int? UserCount { get; init; } /// /// The user type associated with the IP address. This can be one of @@ -506,46 +342,7 @@ public Traits( /// [JsonInclude] [JsonPropertyName("user_type")] - public string? UserType { get; internal set; } - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return $"{nameof(AutonomousSystemNumber)}: {AutonomousSystemNumber}, " + - $"{nameof(AutonomousSystemOrganization)}: {AutonomousSystemOrganization}, " + - $"{nameof(ConnectionType)}: {ConnectionType}, " + - $"{nameof(Domain)}: {Domain}, " + - $"{nameof(IPAddress)}: {IPAddress}, " + - $"{nameof(IpRiskSnapshot)}: {IpRiskSnapshot}, " + -#pragma warning disable 618 - $"{nameof(IsAnonymous)}: {IsAnonymous}, " + - $"{nameof(IsAnonymousProxy)}: {IsAnonymousProxy}, " + - $"{nameof(IsAnonymousVpn)}: {IsAnonymousVpn}, " + -#pragma warning restore 618 - $"{nameof(IsAnycast)}: {IsAnycast}, " + -#pragma warning disable 618 - $"{nameof(IsHostingProvider)}: {IsHostingProvider}, " + -#pragma warning restore 618 - $"{nameof(IsLegitimateProxy)}: {IsLegitimateProxy}, " + -#pragma warning disable 618 - $"{nameof(IsPublicProxy)}: {IsPublicProxy}, " + - $"{nameof(IsResidentialProxy)}: {IsResidentialProxy}, " + - $"{nameof(IsSatelliteProvider)}: {IsSatelliteProvider}, " + - $"{nameof(IsTorExitNode)}: {IsTorExitNode}, " + -#pragma warning restore 618 - $"{nameof(Isp)}: {Isp}, " + - $"{nameof(MobileCountryCode)}: {MobileCountryCode}, " + - $"{nameof(MobileNetworkCode)}: {MobileNetworkCode}, " + - $"{nameof(Network)}: {Network}, " + - $"{nameof(Organization)}: {Organization}, " + - $"{nameof(StaticIPScore)}: {StaticIPScore}, " + - $"{nameof(UserCount)}: {UserCount}, " + - $"{nameof(UserType)}: {UserType}"; - } + [MapKey("user_type")] + public string? UserType { get; init; } } } diff --git a/MaxMind.GeoIP2/Properties/AssemblyInfo.cs b/MaxMind.GeoIP2/Properties/AssemblyInfo.cs index 722707f9..d3ac2b77 100644 --- a/MaxMind.GeoIP2/Properties/AssemblyInfo.cs +++ b/MaxMind.GeoIP2/Properties/AssemblyInfo.cs @@ -51,3 +51,9 @@ "0230946435957d2e52dc0d15673e372248dbff3bc8e6c75a632072e52cb0444850dddff5cc2be8" + "f3e1f8954d7ede7675675a071672d9e97d3153d96b40fd30234be33eeb7fd1a4a78d6342967700" + "56a2b1e5")] +[assembly: InternalsVisibleTo("MaxMind.MinFraud,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100e30b6e4a9425b1" + + "617ffc8bdf79801e67a371f9f650db860dc0dfff92cb63258765a0955c6fcde1da78dbaf5bf84d" + + "0230946435957d2e52dc0d15673e372248dbff3bc8e6c75a632072e52cb0444850dddff5cc2be8" + + "f3e1f8954d7ede7675675a071672d9e97d3153d96b40fd30234be33eeb7fd1a4a78d6342967700" + + "56a2b1e5")] diff --git a/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs b/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs index 74bd225e..38d6739f 100644 --- a/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs +++ b/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs @@ -1,72 +1,39 @@ -#region - +using MaxMind.Db; using MaxMind.GeoIP2.Model; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// Abstract class that city-level response. + /// Abstract record for city-level responses. /// - public abstract class AbstractCityResponse : AbstractCountryResponse + public abstract record AbstractCityResponse : AbstractCountryResponse { - /// - /// Initializes a new instance of the class. - /// - protected AbstractCityResponse() - { - City = new(); - Location = new(); - Postal = new(); - Subdivisions = []; - } - - /// - /// Initializes a new instance of the class. - /// - protected AbstractCityResponse( - City? city = null, - Continent? continent = null, - Country? country = null, - Location? location = null, - Model.MaxMind? maxMind = null, - Postal? postal = null, - Country? registeredCountry = null, - RepresentedCountry? representedCountry = null, - IReadOnlyList? subdivisions = null, - Traits? traits = null) - : base(continent, country, maxMind, registeredCountry, representedCountry, traits) - { - City = city ?? new(); - Location = location ?? new(); - Postal = postal ?? new(); - Subdivisions = subdivisions ?? []; - } - /// /// Gets the city for the requested IP address. /// [JsonInclude] [JsonPropertyName("city")] - public City City { get; internal set; } + [MapKey("city", true)] + public City City { get; init; } = new(); /// /// Gets the location for the requested IP address. /// [JsonInclude] [JsonPropertyName("location")] - public Location Location { get; internal set; } + [MapKey("location", true)] + public Location Location { get; init; } = new(); /// /// Gets the postal object for the requested IP address. /// [JsonInclude] [JsonPropertyName("postal")] - public Postal Postal { get; internal set; } + [MapKey("postal", true)] + public Postal Postal { get; init; } = new(); /// /// An of objects representing @@ -79,7 +46,8 @@ protected AbstractCityResponse( /// [JsonInclude] [JsonPropertyName("subdivisions")] - public IReadOnlyList Subdivisions { get; internal set; } + [MapKey("subdivisions")] + public IReadOnlyList Subdivisions { get; init; } = []; /// /// An object representing the most specific subdivision returned. If @@ -89,47 +57,15 @@ protected AbstractCityResponse( [JsonIgnore] public Subdivision MostSpecificSubdivision => Subdivisions.Count == 0 ? new() : Subdivisions[Subdivisions.Count - 1]; - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() + /// + internal override AbstractResponse WithLocales(IReadOnlyList locales) { - return GetType().Name + " [" - + "City=" + City + ", " - + "Location=" + Location + ", " - + "Postal=" + Postal + ", " - + "Subdivisions={" + - string.Join(",", Subdivisions.Select(s => s.ToString()).ToArray()) + "}, " - + "Continent=" + Continent + ", " - + "Country=" + Country + ", " - + "RegisteredCountry=" + RegisteredCountry + ", " - + "RepresentedCountry=" + RepresentedCountry + ", " - + "Traits=" + Traits - + "]"; - } - - /// - /// Sets the locales on all the NamedEntity properties. - /// - /// The locales specified by the user. - protected internal override void SetLocales(IReadOnlyList locales) - { - locales = [.. locales]; - base.SetLocales(locales); - City.Locales = locales; - - if (Subdivisions.Count == 0) - { - return; - } - - foreach (var subdivision in Subdivisions) + var baseResult = (AbstractCityResponse)base.WithLocales(locales); + return baseResult with { - subdivision.Locales = locales; - } + City = baseResult.City with { Locales = locales }, + Subdivisions = [.. baseResult.Subdivisions.Select(s => s with { Locales = locales })], + }; } } } diff --git a/MaxMind.GeoIP2/Responses/AbstractCountryResponse.cs b/MaxMind.GeoIP2/Responses/AbstractCountryResponse.cs index e2878285..98203181 100644 --- a/MaxMind.GeoIP2/Responses/AbstractCountryResponse.cs +++ b/MaxMind.GeoIP2/Responses/AbstractCountryResponse.cs @@ -1,56 +1,22 @@ -#region - +using MaxMind.Db; using MaxMind.GeoIP2.Model; using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// Abstract class for country-level response. + /// Abstract record for country-level responses. /// - public abstract class AbstractCountryResponse : AbstractResponse + public abstract record AbstractCountryResponse : AbstractResponse { - /// - /// Initializes a new instance of the class. - /// - protected AbstractCountryResponse() - { - Continent = new Continent(); - Country = new Country(); - MaxMind = new Model.MaxMind(); - RegisteredCountry = new Country(); - RepresentedCountry = new RepresentedCountry(); - Traits = new Traits(); - } - - /// - /// Initializes a new instance of the class. - /// - protected AbstractCountryResponse( - Continent? continent = null, - Country? country = null, - Model.MaxMind? maxMind = null, - Country? registeredCountry = null, - RepresentedCountry? representedCountry = null, - Traits? traits = null) - { - Continent = continent ?? new Continent(); - Country = country ?? new Country(); - MaxMind = maxMind ?? new Model.MaxMind(); - RegisteredCountry = registeredCountry ?? new Country(); - RepresentedCountry = representedCountry ?? new RepresentedCountry(); - Traits = traits ?? new Traits(); - } - /// /// Gets the continent for the requested IP address. /// [JsonInclude] [JsonPropertyName("continent")] - public Continent Continent { get; internal set; } + [MapKey("continent", true)] + public Continent Continent { get; init; } = new(); /// /// Gets the country for the requested IP address. This @@ -59,14 +25,15 @@ protected AbstractCountryResponse( /// [JsonInclude] [JsonPropertyName("country")] - public Country Country { get; internal set; } + [MapKey("country", true)] + public Country Country { get; init; } = new(); /// /// Gets the MaxMind record containing data related to your account /// [JsonInclude] [JsonPropertyName("maxmind")] - public Model.MaxMind MaxMind { get; internal set; } + public Model.MaxMind MaxMind { get; init; } = new(); /// /// Registered country record for the requested IP address. This @@ -75,7 +42,8 @@ protected AbstractCountryResponse( /// [JsonInclude] [JsonPropertyName("registered_country")] - public Country RegisteredCountry { get; internal set; } + [MapKey("registered_country", true)] + public Country RegisteredCountry { get; init; } = new(); /// /// Represented country record for the requested IP address. The @@ -85,43 +53,27 @@ protected AbstractCountryResponse( /// [JsonInclude] [JsonPropertyName("represented_country")] - public RepresentedCountry RepresentedCountry { get; internal set; } + [MapKey("represented_country", true)] + public RepresentedCountry RepresentedCountry { get; init; } = new(); /// /// Gets the traits for the requested IP address. /// [JsonInclude] [JsonPropertyName("traits")] - public Traits Traits { get; internal set; } + [MapKey("traits", true)] + public Traits Traits { get; init; } = new(); - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() - { - return GetType().Name + " [" - + "Continent=" + Continent + ", " - + "Country=" + Country + ", " - + "RegisteredCountry=" + RegisteredCountry + ", " - + "RepresentedCountry=" + RepresentedCountry + ", " - + "Traits=" + Traits - + "]"; - } - - /// - /// Sets the locales on all the NamedEntity properties. - /// - /// The locales specified by the user. - protected internal override void SetLocales(IReadOnlyList locales) + /// + internal override AbstractResponse WithLocales(IReadOnlyList locales) { - locales = [.. locales]; - Continent.Locales = locales; - Country.Locales = locales; - RegisteredCountry.Locales = locales; - RepresentedCountry.Locales = locales; + return this with + { + Continent = Continent with { Locales = locales }, + Country = Country with { Locales = locales }, + RegisteredCountry = RegisteredCountry with { Locales = locales }, + RepresentedCountry = RepresentedCountry with { Locales = locales }, + }; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Responses/AbstractResponse.cs b/MaxMind.GeoIP2/Responses/AbstractResponse.cs index 937ba6da..642555e5 100644 --- a/MaxMind.GeoIP2/Responses/AbstractResponse.cs +++ b/MaxMind.GeoIP2/Responses/AbstractResponse.cs @@ -1,22 +1,20 @@ -#region - using System.Collections.Generic; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// Abstract class that represents a generic response. + /// Abstract base record for all responses. /// - public abstract class AbstractResponse + public abstract record AbstractResponse { /// - /// This is simplify the database API. Also, we may need to use the locales in the future. + /// Creates a copy of this response with locales set on all NamedEntity properties. /// - /// - protected internal virtual void SetLocales(IReadOnlyList locales) + /// The locales specified by the user. + /// A new response with the locales set. + internal virtual AbstractResponse WithLocales(IReadOnlyList locales) { + return this; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Responses/AnonymousIPResponse.cs b/MaxMind.GeoIP2/Responses/AnonymousIPResponse.cs index adc05854..35745b66 100644 --- a/MaxMind.GeoIP2/Responses/AnonymousIPResponse.cs +++ b/MaxMind.GeoIP2/Responses/AnonymousIPResponse.cs @@ -1,63 +1,20 @@ -#region - using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoIP2 Anonymous IP response. + /// This record represents the GeoIP2 Anonymous IP response. /// - public class AnonymousIPResponse : AbstractResponse + public record AnonymousIPResponse : AbstractResponse { - /// - /// Construct AnonymousIPResponse model - /// - public AnonymousIPResponse() - { - } - - /// - /// Construct AnonymousIPResponse model - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Constructor] - public AnonymousIPResponse( - [Parameter("is_anonymous")] bool isAnonymous, - [Parameter("is_anonymous_vpn")] bool isAnonymousVpn, - [Parameter("is_hosting_provider")] bool isHostingProvider, - [Parameter("is_public_proxy")] bool isPublicProxy, - [Parameter("is_residential_proxy")] bool isResidentialProxy, - [Parameter("is_tor_exit_node")] bool isTorExitNode, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null - ) - { - IsAnonymous = isAnonymous; - IsAnonymousVpn = isAnonymousVpn; - IsHostingProvider = isHostingProvider; - IsPublicProxy = isPublicProxy; - IsResidentialProxy = isResidentialProxy; - IsTorExitNode = isTorExitNode; - IPAddress = ipAddress; - Network = network; - } - /// /// Returns true if the IP address belongs to any sort of anonymous network. /// [JsonInclude] [JsonPropertyName("is_anonymous")] - public bool IsAnonymous { get; internal set; } + [MapKey("is_anonymous")] + public bool IsAnonymous { get; init; } /// /// Returns true if the IP address is registered to an anonymous @@ -70,7 +27,8 @@ public AnonymousIPResponse( /// [JsonInclude] [JsonPropertyName("is_anonymous_vpn")] - public bool IsAnonymousVpn { get; internal set; } + [MapKey("is_anonymous_vpn")] + public bool IsAnonymousVpn { get; init; } /// /// Returns true if the IP address belongs to a hosting or @@ -78,14 +36,16 @@ public AnonymousIPResponse( /// [JsonInclude] [JsonPropertyName("is_hosting_provider")] - public bool IsHostingProvider { get; internal set; } + [MapKey("is_hosting_provider")] + public bool IsHostingProvider { get; init; } /// /// Returns true if the IP address belongs to a public proxy. /// [JsonInclude] [JsonPropertyName("is_public_proxy")] - public bool IsPublicProxy { get; internal set; } + [MapKey("is_public_proxy")] + public bool IsPublicProxy { get; init; } /// /// This is true if the IP address is on a suspected anonymizing @@ -93,14 +53,16 @@ public AnonymousIPResponse( /// [JsonInclude] [JsonPropertyName("is_residential_proxy")] - public bool IsResidentialProxy { get; internal set; } + [MapKey("is_residential_proxy")] + public bool IsResidentialProxy { get; init; } /// /// Returns true if IP is a Tor exit node. /// [JsonInclude] [JsonPropertyName("is_tor_exit_node")] - public bool IsTorExitNode { get; internal set; } + [MapKey("is_tor_exit_node")] + public bool IsTorExitNode { get; init; } /// /// The IP address that the data in the model is for. If you @@ -111,7 +73,8 @@ public AnonymousIPResponse( /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// The network associated with the record. In particular, this is @@ -120,6 +83,7 @@ public AnonymousIPResponse( /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } + [Network] + public Network? Network { get; init; } } } diff --git a/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs b/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs index 16c59698..a11c40f0 100644 --- a/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs +++ b/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs @@ -1,127 +1,14 @@ -#region - using MaxMind.Db; using System; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoIP Anonymous Plus response. + /// This record represents the GeoIP Anonymous Plus response. /// - public class AnonymousPlusResponse : AnonymousIPResponse + public record AnonymousPlusResponse : AnonymousIPResponse { - /// - /// Construct AnonymousPlusResponse model - /// - public AnonymousPlusResponse() - { - } - -#if NET6_0_OR_GREATER - /// - /// Construct AnonymousPlusResponse model - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Constructor] - public AnonymousPlusResponse( - [Parameter("anonymizer_confidence")] int? anonymizerConfidence, - [Parameter("is_anonymous")] bool isAnonymous, - [Parameter("is_anonymous_vpn")] bool isAnonymousVpn, - [Parameter("is_hosting_provider")] bool isHostingProvider, - [Parameter("is_public_proxy")] bool isPublicProxy, - [Parameter("is_residential_proxy")] bool isResidentialProxy, - [Parameter("is_tor_exit_node")] bool isTorExitNode, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null, - [Parameter("network_last_seen")] string? networkLastSeen = null, - [Parameter("provider_name")] string? providerName = null - ) : this(anonymizerConfidence, isAnonymous, isAnonymousVpn, isHostingProvider, isPublicProxy, - isResidentialProxy, isTorExitNode, ipAddress, network, - networkLastSeen == null ? null : DateOnly.Parse(networkLastSeen), - providerName - ) - { } - - /// - /// Construct AnonymousPlusResponse model - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public AnonymousPlusResponse( - int? anonymizerConfidence, - bool isAnonymous, - bool isAnonymousVpn, - bool isHostingProvider, - bool isPublicProxy, - bool isResidentialProxy, - bool isTorExitNode, - string? ipAddress, - Network? network = null, - DateOnly? networkLastSeen = null, - string? providerName = null - ) : base(isAnonymous, isAnonymousVpn, isHostingProvider, isPublicProxy, - isResidentialProxy, isTorExitNode, ipAddress, network) - { - AnonymizerConfidence = anonymizerConfidence; - NetworkLastSeen = networkLastSeen; - ProviderName = providerName; - } -#else - /// - /// Construct AnonymousPlusResponse model - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Constructor] - public AnonymousPlusResponse( - [Parameter("anonymizer_confidence")] int anonymizerConfidence, - [Parameter("is_anonymous")] bool isAnonymous, - [Parameter("is_anonymous_vpn")] bool isAnonymousVpn, - [Parameter("is_hosting_provider")] bool isHostingProvider, - [Parameter("is_public_proxy")] bool isPublicProxy, - [Parameter("is_residential_proxy")] bool isResidentialProxy, - [Parameter("is_tor_exit_node")] bool isTorExitNode, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null, - [Parameter("provider_name")] string? providerName = null - ) : base(isAnonymous, isAnonymousVpn, isHostingProvider, isPublicProxy, - isResidentialProxy, isTorExitNode, ipAddress, network) - { - AnonymizerConfidence = anonymizerConfidence; - ProviderName = providerName; - } -#endif - /// /// A score ranging from 1 to 99 that is our percent confidence /// that the network is currently part of an actively used VPN @@ -129,16 +16,35 @@ public AnonymousPlusResponse( /// [JsonInclude] [JsonPropertyName("anonymizer_confidence")] - public int? AnonymizerConfidence { get; internal set; } + [MapKey("anonymizer_confidence")] + public int? AnonymizerConfidence { get; init; } #if NET6_0_OR_GREATER + private DateOnly? _networkLastSeen; + /// /// The last day that the network was sighted in our analysis of /// anonymized networks. /// [JsonInclude] [JsonPropertyName("network_last_seen")] - public DateOnly? NetworkLastSeen { get; internal set; } + public DateOnly? NetworkLastSeen + { + get => _networkLastSeen; + init => _networkLastSeen = value; + } + + /// + /// Internal property for MMDB deserialization where the value + /// is stored as a string. + /// + [JsonIgnore] + [MapKey("network_last_seen")] + internal string? NetworkLastSeenString + { + get => _networkLastSeen?.ToString("o"); + init => _networkLastSeen = value == null ? null : DateOnly.Parse(value); + } #endif /// @@ -147,6 +53,7 @@ public AnonymousPlusResponse( /// [JsonInclude] [JsonPropertyName("provider_name")] - public string? ProviderName { get; internal set; } + [MapKey("provider_name")] + public string? ProviderName { get; init; } } } diff --git a/MaxMind.GeoIP2/Responses/AsnResponse.cs b/MaxMind.GeoIP2/Responses/AsnResponse.cs index 35434310..35aad485 100644 --- a/MaxMind.GeoIP2/Responses/AsnResponse.cs +++ b/MaxMind.GeoIP2/Responses/AsnResponse.cs @@ -1,29 +1,13 @@ -using MaxMind.Db; +using MaxMind.Db; using System.Text.Json.Serialization; namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoLite2 ASN response. + /// This record represents the GeoLite2 ASN response. /// - /// - /// Construct an AsnResponse model. - /// - [method: Constructor] - public class AsnResponse( - [Parameter("autonomous_system_number")] long? autonomousSystemNumber, - [Parameter("autonomous_system_organization")] string? autonomousSystemOrganization, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null - ) : AbstractResponse + public record AsnResponse : AbstractResponse { - /// - /// Construct an AsnResponse model. - /// - public AsnResponse() : this(null, null, null) - { - } - /// /// The /// [JsonInclude] [JsonPropertyName("autonomous_system_number")] - public long? AutonomousSystemNumber { get; internal set; } = autonomousSystemNumber; + [MapKey("autonomous_system_number")] + public long? AutonomousSystemNumber { get; init; } /// /// The organization associated with the registered @@ -46,7 +31,8 @@ public AsnResponse() : this(null, null, null) /// [JsonInclude] [JsonPropertyName("autonomous_system_organization")] - public string? AutonomousSystemOrganization { get; internal set; } = autonomousSystemOrganization; + [MapKey("autonomous_system_organization")] + public string? AutonomousSystemOrganization { get; init; } /// /// The IP address that the data in the model is for. If you @@ -57,7 +43,8 @@ public AsnResponse() : this(null, null, null) /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } = ipAddress; + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// The network associated with the record. In particular, this is @@ -66,6 +53,7 @@ public AsnResponse() : this(null, null, null) /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } = network; + [Network] + public Network? Network { get; init; } } } diff --git a/MaxMind.GeoIP2/Responses/CityResponse.cs b/MaxMind.GeoIP2/Responses/CityResponse.cs index 922d063c..5a39ee98 100644 --- a/MaxMind.GeoIP2/Responses/CityResponse.cs +++ b/MaxMind.GeoIP2/Responses/CityResponse.cs @@ -1,45 +1,10 @@ -#region - -using MaxMind.Db; -using MaxMind.GeoIP2.Model; -using System.Collections.Generic; - -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class provides a model for data returned from the GeoIP2 City + /// This record provides a model for data returned from the GeoIP2 City /// database and the GeoIP2 City Plus web services. /// - public class CityResponse : AbstractCityResponse + public record CityResponse : AbstractCityResponse { - /// - /// Constructor - /// - public CityResponse() - { - } - - /// - /// Constructor - /// - [Constructor] - public CityResponse( - City? city = null, - Continent? continent = null, - Country? country = null, - Location? location = null, - [Parameter("maxmind")] Model.MaxMind? maxMind = null, - Postal? postal = null, - [Parameter("registered_country")] Country? registeredCountry = null, - [Parameter("represented_country")] RepresentedCountry? representedCountry = null, - IReadOnlyList? subdivisions = null, - [Parameter("traits", true)] Traits? traits = null) - : base( - city, continent, country, location, maxMind, postal, registeredCountry, representedCountry, subdivisions, - traits) - { - } } } diff --git a/MaxMind.GeoIP2/Responses/ConnectionTypeResponse.cs b/MaxMind.GeoIP2/Responses/ConnectionTypeResponse.cs index 44b9e2a7..fe8f97f4 100644 --- a/MaxMind.GeoIP2/Responses/ConnectionTypeResponse.cs +++ b/MaxMind.GeoIP2/Responses/ConnectionTypeResponse.cs @@ -1,39 +1,13 @@ -#region - using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoIP2 Connection-Type response. + /// This record represents the GeoIP2 Connection-Type response. /// - public class ConnectionTypeResponse : AbstractResponse + public record ConnectionTypeResponse : AbstractResponse { - /// - /// Construct ConnectionTypeResponse model - /// - public ConnectionTypeResponse() - { - } - - /// - /// Construct ConnectionTypeResponse model - /// - [Constructor] - public ConnectionTypeResponse( - [Parameter("connection_type")] string? connectionType, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null - ) - { - ConnectionType = connectionType; - IPAddress = ipAddress; - Network = network; - } - /// /// The connection type may take the following values: "Dialup", /// "Cable/DSL", "Corporate", "Cellular", and "Satellite". Additional @@ -41,7 +15,8 @@ public ConnectionTypeResponse( /// [JsonInclude] [JsonPropertyName("connection_type")] - public string? ConnectionType { get; internal set; } + [MapKey("connection_type")] + public string? ConnectionType { get; init; } /// /// The IP address that the data in the model is for. If you @@ -52,7 +27,8 @@ public ConnectionTypeResponse( /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// The network associated with the record. In particular, this is @@ -61,6 +37,7 @@ public ConnectionTypeResponse( /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } + [Network] + public Network? Network { get; init; } } -} \ No newline at end of file +} diff --git a/MaxMind.GeoIP2/Responses/CountryResponse.cs b/MaxMind.GeoIP2/Responses/CountryResponse.cs index 4e53d975..d79025de 100644 --- a/MaxMind.GeoIP2/Responses/CountryResponse.cs +++ b/MaxMind.GeoIP2/Responses/CountryResponse.cs @@ -1,38 +1,10 @@ -#region - -using MaxMind.Db; -using MaxMind.GeoIP2.Model; - -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class provides a model for the data returned from the GeoIP2 + /// This record provides a model for the data returned from the GeoIP2 /// Country database and the GeoIP2 Country web service. /// - public class CountryResponse : AbstractCountryResponse + public record CountryResponse : AbstractCountryResponse { - /// - /// Constructor - /// - public CountryResponse() - { - } - - /// - /// Constructor - /// - [Constructor] - public CountryResponse( - Continent? continent = null, - Country? country = null, - [Parameter("maxmind")] Model.MaxMind? maxMind = null, - [Parameter("registered_country")] Country? registeredCountry = null, - [Parameter("represented_country")] RepresentedCountry? representedCountry = null, - [Parameter("traits", true)] Traits? traits = null - ) : base(continent, country, maxMind, registeredCountry, representedCountry, traits) - { - } } } diff --git a/MaxMind.GeoIP2/Responses/DomainResponse.cs b/MaxMind.GeoIP2/Responses/DomainResponse.cs index b046f17f..d520392c 100644 --- a/MaxMind.GeoIP2/Responses/DomainResponse.cs +++ b/MaxMind.GeoIP2/Responses/DomainResponse.cs @@ -1,35 +1,13 @@ -#region - using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoIP2 Domain response. + /// This record represents the GeoIP2 Domain response. /// - /// - /// Construct a DomainResponse model object. - /// - /// - /// - /// - [method: Constructor] - public class DomainResponse( - string? domain, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null - ) : AbstractResponse + public record DomainResponse : AbstractResponse { - /// - /// Construct a DomainResponse model object. - /// - public DomainResponse() : this(null, null) - { - } - /// /// The second level domain associated with the IP address. This will /// be something like "example.com" or "example.co.uk", not @@ -37,7 +15,8 @@ public DomainResponse() : this(null, null) /// [JsonInclude] [JsonPropertyName("domain")] - public string? Domain { get; internal set; } = domain; + [MapKey("domain")] + public string? Domain { get; init; } /// /// The IP address that the data in the model is for. If you @@ -48,7 +27,8 @@ public DomainResponse() : this(null, null) /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } = ipAddress; + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// The network associated with the record. In particular, this is @@ -57,6 +37,7 @@ public DomainResponse() : this(null, null) /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } = network; + [Network] + public Network? Network { get; init; } } } diff --git a/MaxMind.GeoIP2/Responses/EnterpriseResponse.cs b/MaxMind.GeoIP2/Responses/EnterpriseResponse.cs index 2d189631..9c433357 100644 --- a/MaxMind.GeoIP2/Responses/EnterpriseResponse.cs +++ b/MaxMind.GeoIP2/Responses/EnterpriseResponse.cs @@ -1,34 +1,10 @@ -#region - -using MaxMind.Db; -using MaxMind.GeoIP2.Model; -using System.Collections.Generic; - -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class provides a model for the data returned by the GeoIP2 Enterprise + /// This record provides a model for the data returned by the GeoIP2 Enterprise /// database. /// - /// - /// Constructor - /// - [method: Constructor] - public class EnterpriseResponse( - City? city = null, - Continent? continent = null, - Country? country = null, - Location? location = null, - Model.MaxMind? maxMind = null, - Postal? postal = null, - Country? registeredCountry = null, - RepresentedCountry? representedCountry = null, - IReadOnlyList? subdivisions = null, - Traits? traits = null) : AbstractCityResponse( - city, continent, country, location, maxMind, postal, registeredCountry, representedCountry, subdivisions, - traits) + public record EnterpriseResponse : AbstractCityResponse { } } diff --git a/MaxMind.GeoIP2/Responses/InsightsResponse.cs b/MaxMind.GeoIP2/Responses/InsightsResponse.cs index 81758e44..01a3af2f 100644 --- a/MaxMind.GeoIP2/Responses/InsightsResponse.cs +++ b/MaxMind.GeoIP2/Responses/InsightsResponse.cs @@ -1,86 +1,20 @@ -#region - using MaxMind.GeoIP2.Model; -using System; -using System.Collections.Generic; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class provides a model for the data returned by the GeoIP2 + /// This record provides a model for the data returned by the GeoIP2 /// Insights web service. /// - public class InsightsResponse : AbstractCityResponse + public record InsightsResponse : AbstractCityResponse { - /// - /// Constructor - /// - public InsightsResponse() - { - Anonymizer = new Anonymizer(); - } - - /// - /// Constructor - /// - public InsightsResponse( - Anonymizer? anonymizer = null, - City? city = null, - Continent? continent = null, - Country? country = null, - Location? location = null, - Model.MaxMind? maxMind = null, - Postal? postal = null, - Country? registeredCountry = null, - RepresentedCountry? representedCountry = null, - IReadOnlyList? subdivisions = null, - Traits? traits = null) - : base( - city, continent, country, location, maxMind, postal, registeredCountry, representedCountry, subdivisions, - traits) - { - Anonymizer = anonymizer ?? new Anonymizer(); - } - - /// - /// Constructor for backward compatibility - /// - [Obsolete("Use constructor with anonymizer parameter")] - public InsightsResponse( - City? city, - Continent? continent, - Country? country, - Location? location, - Model.MaxMind? maxMind, - Postal? postal, - Country? registeredCountry, - RepresentedCountry? representedCountry, - IReadOnlyList? subdivisions, - Traits? traits) - : this( - null, // anonymizer - city, - continent, - country, - location, - maxMind, - postal, - registeredCountry, - representedCountry, - subdivisions, - traits) - { - } - /// /// Gets anonymizer-related data for the requested IP address. /// This is available from the GeoIP2 Insights web service. /// [JsonInclude] [JsonPropertyName("anonymizer")] - public Anonymizer Anonymizer { get; internal set; } + public Anonymizer Anonymizer { get; init; } = new(); } } diff --git a/MaxMind.GeoIP2/Responses/IspResponse.cs b/MaxMind.GeoIP2/Responses/IspResponse.cs index 23fa3522..96e20536 100644 --- a/MaxMind.GeoIP2/Responses/IspResponse.cs +++ b/MaxMind.GeoIP2/Responses/IspResponse.cs @@ -1,37 +1,13 @@ -#region - using MaxMind.Db; using System.Text.Json.Serialization; -#endregion - namespace MaxMind.GeoIP2.Responses { /// - /// This class represents the GeoIP2 ISP response. + /// This record represents the GeoIP2 ISP response. /// - /// - /// Construct an IspResponse model. - /// - [method: Constructor] - public class IspResponse( - [Parameter("autonomous_system_number")] long? autonomousSystemNumber, - [Parameter("autonomous_system_organization")] string? autonomousSystemOrganization, - string? isp, - [Parameter("mobile_country_code")] string? mobileCountryCode, - [Parameter("mobile_network_code")] string? mobileNetworkCode, - string? organization, - [Inject("ip_address")] string? ipAddress, - [Network] Network? network = null - ) : AbstractResponse + public record IspResponse : AbstractResponse { - /// - /// Construct an IspResponse model. - /// - public IspResponse() : this(null, null, null, null, null, null, null) - { - } - /// /// The /// [JsonInclude] [JsonPropertyName("autonomous_system_number")] - public long? AutonomousSystemNumber { get; internal set; } = autonomousSystemNumber; + [MapKey("autonomous_system_number")] + public long? AutonomousSystemNumber { get; init; } /// /// The organization associated with the registered @@ -54,14 +31,16 @@ public IspResponse() : this(null, null, null, null, null, null, null) /// [JsonInclude] [JsonPropertyName("autonomous_system_organization")] - public string? AutonomousSystemOrganization { get; internal set; } = autonomousSystemOrganization; + [MapKey("autonomous_system_organization")] + public string? AutonomousSystemOrganization { get; init; } /// /// The name of the ISP associated with the IP address. /// [JsonInclude] [JsonPropertyName("isp")] - public string? Isp { get; internal set; } = isp; + [MapKey("isp")] + public string? Isp { get; init; } /// /// The @@ -69,7 +48,8 @@ public IspResponse() : this(null, null, null, null, null, null, null) /// [JsonInclude] [JsonPropertyName("mobile_country_code")] - public string? MobileCountryCode { get; internal set; } = mobileCountryCode; + [MapKey("mobile_country_code")] + public string? MobileCountryCode { get; init; } /// /// The @@ -77,14 +57,16 @@ public IspResponse() : this(null, null, null, null, null, null, null) /// [JsonInclude] [JsonPropertyName("mobile_network_code")] - public string? MobileNetworkCode { get; internal set; } = mobileNetworkCode; + [MapKey("mobile_network_code")] + public string? MobileNetworkCode { get; init; } /// /// The name of the organization associated with the IP address. /// [JsonInclude] [JsonPropertyName("organization")] - public string? Organization { get; internal set; } = organization; + [MapKey("organization")] + public string? Organization { get; init; } /// /// The IP address that the data in the model is for. If you @@ -95,7 +77,8 @@ public IspResponse() : this(null, null, null, null, null, null, null) /// [JsonInclude] [JsonPropertyName("ip_address")] - public string? IPAddress { get; internal set; } = ipAddress; + [Inject("ip_address")] + public string? IPAddress { get; init; } /// /// The network associated with the record. In particular, this is @@ -104,6 +87,7 @@ public IspResponse() : this(null, null, null, null, null, null, null) /// [JsonInclude] [JsonPropertyName("network")] - public Network? Network { get; internal set; } = network; + [Network] + public Network? Network { get; init; } } } diff --git a/MaxMind.GeoIP2/WebServiceClient.cs b/MaxMind.GeoIP2/WebServiceClient.cs index 4b639d5d..d2ca26ff 100644 --- a/MaxMind.GeoIP2/WebServiceClient.cs +++ b/MaxMind.GeoIP2/WebServiceClient.cs @@ -427,8 +427,7 @@ private T CreateModel(Response response) throw new HttpException( $"Received a 200 response for {response.RequestUri} but there was no message body.", HttpStatusCode.OK, response.RequestUri); - model.SetLocales(_locales); - return model; + return (T)model.WithLocales(_locales); } catch (JsonException ex) { diff --git a/releasenotes.md b/releasenotes.md index 75d6bdd4..d27f3dd0 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,6 +1,32 @@ GeoIP2 .NET API Release Notes ============================= +6.0.0 +------------------ + +* **BREAKING:** All model and response classes have been converted from + classes to C# records. +* **BREAKING:** Constructor-based deserialization replaced with + property-based initialization using `[MapKey]` attributes. The + `[Constructor]` and `[Parameter]` attributes are no longer used on + GeoIP2 model classes. +* **BREAKING:** `SetLocales()` replaced with `WithLocales()` which returns + a new instance via record `with` expressions instead of mutating in + place. +* **BREAKING:** Properties now use `init` setters instead of + `internal set`. +* **BREAKING:** The `Locales` property on `NamedEntity` (and all subclasses) + is now `public` (previously `protected internal`). This is required for + the record `with` expression pattern. +* **BREAKING:** Code that constructs model objects using constructor + parameters (e.g., `new Traits(domain: "example.com")`) must switch to + object initializer syntax (e.g., `new Traits { Domain = "example.com" }`). +* **BREAKING:** The custom `ToString()` on `NamedEntity` (and all + subclasses: City, Country, Continent, Subdivision, RepresentedCountry) + has been removed. Records provide a compiler-generated `ToString()` that + outputs all property values. Use the `Name` property directly instead. +* Added `InternalsVisibleTo` for `MaxMind.MinFraud` assembly. + 5.5.0 ------------------ From 8387730d531d1c103f4c91dbc209cc985cc3ed18 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 07:58:10 -0700 Subject: [PATCH 02/15] Add mise config to manage .NET SDK versions Co-Authored-By: Claude Opus 4.6 --- mise.lock | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ mise.toml | 20 +++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 mise.lock create mode 100644 mise.toml diff --git a/mise.lock b/mise.lock new file mode 100644 index 00000000..6cafa624 --- /dev/null +++ b/mise.lock @@ -0,0 +1,88 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +[[tools.dotnet]] +version = "10.0.201" +backend = "core:dotnet" + +[[tools.dotnet]] +version = "8.0.419" +backend = "core:dotnet" + +[[tools.dotnet]] +version = "9.0.312" +backend = "core:dotnet" + +[[tools."github:houseabsolute/precious"]] +version = "0.10.2" +backend = "github:houseabsolute/precious" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64"] +checksum = "sha256:8fbaead9f9626170549c3121e67d1bc81193b3bb086e29576f548aefa839fcc4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520042" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64-musl"] +checksum = "sha256:8fbaead9f9626170549c3121e67d1bc81193b3bb086e29576f548aefa839fcc4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520042" + +[tools."github:houseabsolute/precious"."platforms.linux-x64"] +checksum = "sha256:3d717d906db338f63017766b07982dc9055773e1b3bec6d3f432d1f0ad9676bb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519861" + +[tools."github:houseabsolute/precious"."platforms.linux-x64-musl"] +checksum = "sha256:3d717d906db338f63017766b07982dc9055773e1b3bec6d3f432d1f0ad9676bb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519861" + +[tools."github:houseabsolute/precious"."platforms.macos-arm64"] +checksum = "sha256:04157c64459bb6ab029295b21b112077040ad2575b34508d84b19a839551cddb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-macOS-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519985" + +[tools."github:houseabsolute/precious"."platforms.macos-x64"] +checksum = "sha256:9932defd246d0771530357463bdb55582557fd7381853cb4dc2074e36ad0cc84" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-macOS-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519772" + +[tools."github:houseabsolute/precious"."platforms.windows-x64"] +checksum = "sha256:9d683d1730e302c646ccb90a23d313e7a548c8b23b5abf7d24e19ff6befe763d" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Windows-msvc-x86_64.zip" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520544" + +[[tools.node]] +version = "25.8.0" +backend = "core:node" + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:54c128f5286a4392a1fd1c765729b074a6873abff8a4f9bb3d63d571c2855e41" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:54c128f5286a4392a1fd1c765729b074a6873abff8a4f9bb3d63d571c2855e41" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:2ae6f70d74a459c0a96456e486dc60f3e7e65d7752ad302771834e58b27500af" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:2ae6f70d74a459c0a96456e486dc60f3e7e65d7752ad302771834e58b27500af" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-linux-x64.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:75ff6fd07e0a85fb4d2529f6189c996014b1d3d83180c31e65feb2b3eaeec5d9" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:03fb559600c3ede0228d8b588ac6ad8b7b2cd0bac9422b56e7e2ef7f5c11b67c" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:5744746371a417179a701044739b5fa2b3164e943aa57f86059fb312f8032e86" +url = "https://nodejs.org/dist/v25.8.0/node-v25.8.0-win-x64.zip" + +[[tools."npm:prettier"]] +version = "3.8.1" +backend = "npm:prettier" diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..234f6553 --- /dev/null +++ b/mise.toml @@ -0,0 +1,20 @@ +[settings] +experimental = true +lockfile = true +disable_backends = [ + "asdf", + "vfox", +] + +[tools] +dotnet = ["latest", "9", "8"] +"github:houseabsolute/precious" = "latest" +node = "latest" +"npm:prettier" = "latest" + +[hooks] +enter = "mise install --quiet --locked" + +[[watch_files]] +patterns = ["mise.toml", "mise.lock"] +run = "mise install --quiet --locked" From 4543ea1385b9edeb767b6eb9dd616ae58b2918f5 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 07:58:43 -0700 Subject: [PATCH 03/15] Add precious and prettier config with GitHub Action Co-Authored-By: Claude Opus 4.6 --- .github/workflows/precious.yml | 29 +++++++++++++++++++++++++++++ .precious.toml | 32 ++++++++++++++++++++++++++++++++ .prettierrc.json | 6 ++++++ 3 files changed, 67 insertions(+) create mode 100644 .github/workflows/precious.yml create mode 100644 .precious.toml create mode 100644 .prettierrc.json diff --git a/.github/workflows/precious.yml b/.github/workflows/precious.yml new file mode 100644 index 00000000..867f4b02 --- /dev/null +++ b/.github/workflows/precious.yml @@ -0,0 +1,29 @@ +name: precious + +on: + push: + pull_request: + schedule: + - cron: "5 5 * * SUN" + +permissions: {} + +jobs: + precious: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup mise + uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1 + with: + cache: true + env: + # Multiple dotnet SDKs share a single dotnet-root. Parallel + # installs race on the shared binary, causing verification + # failures (exit code 150). + MISE_JOBS: "1" + - name: Run precious lint + run: precious lint --all diff --git a/.precious.toml b/.precious.toml new file mode 100644 index 00000000..ed6bac46 --- /dev/null +++ b/.precious.toml @@ -0,0 +1,32 @@ +exclude = [ + ".git", + "MaxMind.GeoIP2.UnitTests/TestData/MaxMind-DB/**", + "BenchmarkDotNet.Artifacts/**", +] + +[commands.prettier-markdown] +type = "both" +cmd = ["prettier", "--prose-wrap", "always"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = "**/*.md" +ok-exit-codes = 0 + +[commands.prettier-json] +type = "both" +cmd = ["prettier"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = "**/*.json" +ok-exit-codes = 0 + +[commands.prettier-yaml] +type = "both" +cmd = ["prettier"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = ["**/*.yml", "**/*.yaml"] +ok-exit-codes = 0 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..70ac1753 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "proseWrap": "always", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false +} From a1fd996aeb672690de86643257952efc44ba4733 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 08:00:23 -0700 Subject: [PATCH 04/15] Run prettier on existing markdown and YAML files Co-Authored-By: Claude Opus 4.6 --- .github/dependabot.yml | 6 +- .github/workflows/codeql-analysis.yml | 95 +++-- .github/workflows/release.yml | 3 +- .github/workflows/test.yml | 13 +- CLAUDE.md | 99 +++-- README.dev.md | 3 +- README.md | 199 +++++---- releasenotes.md | 572 ++++++++++++-------------- 8 files changed, 489 insertions(+), 501 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf54de77..df05ac80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,10 +2,10 @@ version: 2 updates: - package-ecosystem: nuget directories: - - '**/*' + - "**/*" schedule: interval: daily - time: '14:00' + time: "14:00" open-pull-requests-limit: 10 cooldown: default-days: 7 @@ -13,6 +13,6 @@ updates: directory: / schedule: interval: daily - time: '14:00' + time: "14:00" cooldown: default-days: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 824ad8b1..423629e2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,62 +3,61 @@ name: "Code scanning - action" on: push: branches-ignore: - - 'dependabot/**' + - "dependabot/**" pull_request: schedule: - - cron: '0 1 * * 1' + - cron: "0 1 * * 1" jobs: CodeQL-Build: - runs-on: ubuntu-latest permissions: security-events: write steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - persist-credentials: false - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - - name: Setup .NET - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: | - 8.0.x - 9.0.x - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + persist-credentials: false + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + - name: Setup .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: | + 8.0.x + 9.0.x + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8c93a38..9a1ab24d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,8 @@ jobs: - name: Run tests run: dotnet test -c Release env: - MAXMIND_TEST_BASE_DIR: ${{ github.workspace }}/MaxMind.GeoIP2.UnitTests + MAXMIND_TEST_BASE_DIR: + ${{ github.workspace }}/MaxMind.GeoIP2.UnitTests - name: Pack run: dotnet pack -c Release MaxMind.GeoIP2/MaxMind.GeoIP2.csproj diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6515a63..98f1cd70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: push: pull_request: schedule: - - cron: '3 20 * * SUN' + - cron: "3 20 * * SUN" permissions: {} @@ -32,11 +32,16 @@ jobs: run: dotnet build - name: Run benchmark - run: dotnet run -f net8.0 -p MaxMind.GeoIP2.Benchmark/MaxMind.GeoIP2.Benchmark.csproj + run: + dotnet run -f net8.0 -p + MaxMind.GeoIP2.Benchmark/MaxMind.GeoIP2.Benchmark.csproj env: - MAXMIND_BENCHMARK_DB: ${{ github.workspace }}/MaxMind.GeoIP2.UnitTests/TestData/MaxMind-DB/test-data/GeoIP2-City-Test.mmdb + MAXMIND_BENCHMARK_DB: + ${{ github.workspace + }}/MaxMind.GeoIP2.UnitTests/TestData/MaxMind-DB/test-data/GeoIP2-City-Test.mmdb - name: Run tests run: dotnet test env: - MAXMIND_TEST_BASE_DIR: ${{ github.workspace }}/MaxMind.GeoIP2.UnitTests + MAXMIND_TEST_BASE_DIR: + ${{ github.workspace }}/MaxMind.GeoIP2.UnitTests diff --git a/CLAUDE.md b/CLAUDE.md index 380b671c..05338d30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,16 +1,22 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with +code in this repository. ## Project Overview **GeoIP2-dotnet** is MaxMind's official .NET client library for: + - **GeoIP2/GeoLite2 Web Services**: Country, City, and Insights endpoints -- **GeoIP2/GeoLite2 Databases**: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.) +- **GeoIP2/GeoLite2 Databases**: Local MMDB file reading for various database + types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.) -The library provides both web service clients and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data. +The library provides both web service clients and database readers that return +strongly-typed model objects containing geographic, ISP, anonymizer, and other +IP-related data. **Key Technologies:** + - .NET 10.0, .NET 9.0, .NET 8.0, .NET Standard 2.1, and .NET Standard 2.0 - System.Text.Json for JSON serialization/deserialization - MaxMind.Db library for binary database file reading @@ -42,7 +48,8 @@ MaxMind.GeoIP2.UnitTests/ #### 1. **Immutable Model Classes with Optional Parameters** -Model and response classes use C# classes (not records) with properties and constructors that accept optional parameters with defaults: +Model and response classes use C# classes (not records) with properties and +constructors that accept optional parameters with defaults: ```csharp public class Traits @@ -65,15 +72,18 @@ public class Traits ``` **Key Points:** + - Use `[JsonPropertyName]` for JSON field mapping (System.Text.Json) - Use `[Parameter]` for MaxMind DB field mapping - Use `[Constructor]` attribute for MaxMind DB deserialization -- Properties use `internal set` to prevent external modification while allowing deserialization +- Properties use `internal set` to prevent external modification while allowing + deserialization - Default parameters in constructors avoid breaking changes #### 2. **Conditional Compilation for .NET Version Differences** -Some features are only available in newer .NET versions (e.g., `DateOnly` in .NET 6+): +Some features are only available in newer .NET versions (e.g., `DateOnly` in +.NET 6+): ```csharp #if NET6_0_OR_GREATER @@ -83,7 +93,8 @@ Some features are only available in newer .NET versions (e.g., `DateOnly` in .NE #endif ``` -When adding features that use newer .NET types, ensure backward compatibility with .NET Standard 2.0/2.1. +When adding features that use newer .NET types, ensure backward compatibility +with .NET Standard 2.0/2.1. #### 3. **Deprecation Strategy** @@ -94,20 +105,26 @@ When deprecating properties, mark them with `[Obsolete]` and provide guidance: public int? MetroCode { get; internal set; } ``` -For backward compatibility during minor version updates, add deprecated constructors that match old signatures (see "Avoiding Breaking Changes in Minor Versions"). +For backward compatibility during minor version updates, add deprecated +constructors that match old signatures (see "Avoiding Breaking Changes in Minor +Versions"). #### 4. **Database vs Web Service Architecture** **Database Reader:** + - Reads binary MMDB files using `MaxMind.Db` library - Methods may throw `AddressNotFoundException` or `InvalidOperationException` -- Support for multiple database types via specific methods (`City()`, `Country()`, `AnonymousIP()`, etc.) +- Support for multiple database types via specific methods (`City()`, + `Country()`, `AnonymousIP()`, etc.) - Thread-safe, should be reused across lookups **Web Service Client:** + - Uses `HttpClient` for HTTP requests - Methods throw `GeoIP2Exception` or subclasses on errors -- Supports custom timeouts, locales, host configuration, and dependency injection +- Supports custom timeouts, locales, host configuration, and dependency + injection - Thread-safe, connection pooling via client reuse - Supports both sync and async methods @@ -122,15 +139,19 @@ Classes that are deserialized from MMDB databases use special attributes: #### 6. **ASP.NET Core Integration** -The library supports dependency injection via `IOptions` and `IHttpClientFactory` pattern for typed clients. Configuration is done via `appsettings.json`. +The library supports dependency injection via +`IOptions` and `IHttpClientFactory` pattern for typed +clients. Configuration is done via `appsettings.json`. ## Testing Conventions ### Test Structure - Tests use xUnit framework -- Test databases are in `MaxMind.GeoIP2.UnitTests/TestData/MaxMind-DB/` (git submodule) -- Environment variable `MAXMIND_TEST_BASE_DIR` must point to the test project directory +- Test databases are in `MaxMind.GeoIP2.UnitTests/TestData/MaxMind-DB/` (git + submodule) +- Environment variable `MAXMIND_TEST_BASE_DIR` must point to the test project + directory ### Running Tests @@ -153,7 +174,9 @@ dotnet run -f net8.0 -p MaxMind.GeoIP2.Benchmark/MaxMind.GeoIP2.Benchmark.csproj ### Test Fixtures When adding new fields to responses: -1. Verify test databases in `TestData/MaxMind-DB/test-data/` include the new fields + +1. Verify test databases in `TestData/MaxMind-DB/test-data/` include the new + fields 2. Update test assertions in corresponding test classes 3. Test both database reader and web service client paths @@ -162,6 +185,7 @@ When adding new fields to responses: ### Adding New Fields to Existing Models 1. **Add the property** with appropriate attributes: + ```csharp [JsonInclude] [JsonPropertyName("field_name")] @@ -170,6 +194,7 @@ When adding new fields to responses: ``` 2. **Update constructor(s)** to include the new parameter with a default value: + ```csharp public ModelClass( // ... existing parameters ... @@ -181,7 +206,8 @@ When adding new fields to responses: } ``` -3. **For minor version releases**: Add a deprecated constructor matching the old signature to avoid breaking changes (see next section) +3. **For minor version releases**: Add a deprecated constructor matching the old + signature to avoid breaking changes (see next section) 4. **Update tests** with assertions for the new field @@ -189,7 +215,9 @@ When adding new fields to responses: ### Avoiding Breaking Changes in Minor Versions -When adding a new field to an existing model class during a **minor version release** (e.g., 5.x.0 → 5.y.0), maintain backward compatibility for users constructing these models directly. +When adding a new field to an existing model class during a **minor version +release** (e.g., 5.x.0 → 5.y.0), maintain backward compatibility for users +constructing these models directly. **The Solution:** Add a deprecated constructor that matches the old signature: @@ -219,16 +247,20 @@ public class Traits } ``` -**For Major Versions:** You do NOT need to add the deprecated constructor - breaking changes are expected in major version bumps (e.g., 5.x.0 → 6.0.0). +**For Major Versions:** You do NOT need to add the deprecated constructor - +breaking changes are expected in major version bumps (e.g., 5.x.0 → 6.0.0). ### Adding New Response Types When creating a new response class (e.g., for a new database type): -1. **Determine if it extends existing response** (e.g., `AbstractCityResponse`, `AbstractCountryResponse`, `AbstractResponse`) +1. **Determine if it extends existing response** (e.g., `AbstractCityResponse`, + `AbstractCountryResponse`, `AbstractResponse`) 2. **Follow patterns** from similar responses -3. **Add corresponding database reader method** in `DatabaseReader.cs` and `IGeoIP2DatabaseReader.cs` -4. **Add corresponding web service method** (if applicable) in `WebServiceClient.cs` and `IGeoIP2WebServicesClient.cs` +3. **Add corresponding database reader method** in `DatabaseReader.cs` and + `IGeoIP2DatabaseReader.cs` +4. **Add corresponding web service method** (if applicable) in + `WebServiceClient.cs` and `IGeoIP2WebServicesClient.cs` 5. **Provide comprehensive XML documentation** for all public members 6. **Add unit tests** in `DatabaseReaderTests.cs` or `WebServiceClientTests.cs` @@ -246,21 +278,23 @@ When deprecating fields or methods: Always update `releasenotes.md` for user-facing changes: ```markdown -5.4.0 (YYYY-MM-DD) ------------------- - -* A new `PropertyName` property has been added to `MaxMind.GeoIP2.Model.ModelClass`. - This provides information about... -* The `OldProperty` property in `MaxMind.GeoIP2.Model.ModelClass` has been marked - `Obsolete`. Please use `NewProperty` instead. -* **BREAKING:** Description of any breaking changes (major versions only). +## 5.4.0 (YYYY-MM-DD) + +- A new `PropertyName` property has been added to + `MaxMind.GeoIP2.Model.ModelClass`. This provides information about... +- The `OldProperty` property in `MaxMind.GeoIP2.Model.ModelClass` has been + marked `Obsolete`. Please use `NewProperty` instead. +- **BREAKING:** Description of any breaking changes (major versions only). ``` ### Multi-threaded Safety -Both `DatabaseReader` and `WebServiceClient` are **thread-safe** and should be reused: +Both `DatabaseReader` and `WebServiceClient` are **thread-safe** and should be +reused: + - Create once, share across threads -- Reusing clients improves performance (connection pooling for web service client) +- Reusing clients improves performance (connection pooling for web service + client) - Document thread-safety in XML documentation for all client classes ## Common Patterns @@ -332,7 +366,8 @@ The project enforces strict code quality standards: ### Code Style - Use the `.editorconfig` settings for consistent formatting -- Follow C# naming conventions (PascalCase for public members, camelCase for parameters) +- Follow C# naming conventions (PascalCase for public members, camelCase for + parameters) - Use XML documentation for all public types and members - Keep constructors organized with optional parameters and defaults @@ -355,4 +390,4 @@ The project enforces strict code quality standards: --- -*Last Updated: 2025-11-06* +_Last Updated: 2025-11-06_ diff --git a/README.dev.md b/README.dev.md index 04b06a49..6217c156 100644 --- a/README.dev.md +++ b/README.dev.md @@ -14,4 +14,5 @@ 4. Run dev-bin/release.sh. This will build the project, generate docs, upload to NuGet, and make a GitHub release. 5. Update GitHub Release page for the release. -6. Verify the release on [NuGet](https://www.nuget.org/packages/MaxMind.GeoIP2/). +6. Verify the release on + [NuGet](https://www.nuget.org/packages/MaxMind.GeoIP2/). diff --git a/README.md b/README.md index b2b3246e..386f62a7 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ -# GeoIP2 .NET API # +# GeoIP2 .NET API [![NuGet](https://img.shields.io/nuget/v/MaxMind.GeoIP2)](https://www.nuget.org/packages/MaxMind.GeoIP2) -## Description ## +## Description This distribution provides an API for the GeoIP2 and GeoLite2 [web services](https://dev.maxmind.com/geoip/docs/web-services?lang=en) and [databases](https://dev.maxmind.com/geoip/docs/databases?lang=en). -## Installation ## +## Installation -### NuGet ### +### NuGet -We recommend installing this library with NuGet. To do this, type the -following into the Visual Studio Package Manager Console: +We recommend installing this library with NuGet. To do this, type the following +into the Visual Studio Package Manager Console: ``` install-package MaxMind.GeoIP2 ``` -## IP Geolocation Usage ## +## IP Geolocation Usage IP geolocation is inherently imprecise. Locations are often near the center of -the population. Any location provided by a GeoIP2 database or web service -should not be used to identify a particular address or household. +the population. Any location provided by a GeoIP2 database or web service should +not be used to identify a particular address or household. -## Web Service Usage ## +## Web Service Usage -To use the web service API, first create a new `WebServiceClient` object -with your account ID and license key: +To use the web service API, first create a new `WebServiceClient` object with +your account ID and license key: ``` var client = new WebServiceClient(42, "license_key1"); @@ -47,26 +47,26 @@ To query the Sandbox GeoIP2 web service, you must set the host to var client = new WebServiceClient(42, "license_key1", host: "sandbox.maxmind.com"); ``` -You may also specify the fall-back locales, the host, or the timeout as -optional parameters. See the API docs for more information. +You may also specify the fall-back locales, the host, or the timeout as optional +parameters. See the API docs for more information. This object is safe to share across threads. If you are making multiple -requests, the object should be reused so that new connections are not -created for each request. Once you have finished making requests, you -should dispose of the object to ensure the connections are closed and any -resources are promptly returned to the system. +requests, the object should be reused so that new connections are not created +for each request. Once you have finished making requests, you should dispose of +the object to ensure the connections are closed and any resources are promptly +returned to the system. You may then call the sync or async method corresponding to the specific end point, passing it the IP address you want to look up or no parameters if you want to look up the current device. If the request succeeds, the method call will return a response class for the -endpoint you called. This response in turn contains multiple model classes, -each of which represents part of the data returned by the web service. +endpoint you called. This response in turn contains multiple model classes, each +of which represents part of the data returned by the web service. See the API documentation for more details. -### ASP.NET Core Usage ### +### ASP.NET Core Usage To use the web service API with HttpClient factory pattern as a [Typed client](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#typed-clients) @@ -82,7 +82,8 @@ builder.Services.Configure(builder.Configuration.GetSec builder.Services.AddHttpClient(); ``` -2. Add configuration in your `appsettings.json` with your account ID and license key. +2. Add configuration in your `appsettings.json` with your account ID and license + key. ```jsonc ... @@ -127,9 +128,9 @@ public class MaxMindController : ControllerBase } ``` -## Web Service Example ## +## Web Service Example -### Country Service (Sync) ### +### Country Service (Sync) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -152,7 +153,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -### Country Service (Async) ### +### Country Service (Async) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -175,7 +176,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -### City Plus Service (Sync) ### +### City Plus Service (Sync) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -208,7 +209,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -### City Plus Service (Async) ### +### City Plus Service (Async) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -241,7 +242,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -### Insights Service (Sync) ### +### Insights Service (Sync) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -273,7 +274,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -### Insights Service (Async) ### +### Insights Service (Async) ```csharp // If you are making multiple requests, a single WebServiceClient @@ -305,7 +306,7 @@ using (var client = new WebServiceClient(42, "license_key")) } ``` -## Database Usage ## +## Database Usage To use the database API, you must create a new `DatabaseReader` with a string representation of the path to your GeoIP2 database. You may also specify the @@ -313,18 +314,18 @@ file access mode. You may then call the appropriate method (e.g., `city`) for your database, passing it the IP address you want to look up. If the lookup succeeds, the method call will return a response class for the -GeoIP2 lookup. This class in turn contains multiple model classes, each of -which represents part of the data returned by the database. +GeoIP2 lookup. This class in turn contains multiple model classes, each of which +represents part of the data returned by the database. -We recommend reusing the `DatabaseReader` object rather than creating a new -one for each lookup. The creation of this object is relatively expensive as it -must read in metadata for the file. +We recommend reusing the `DatabaseReader` object rather than creating a new one +for each lookup. The creation of this object is relatively expensive as it must +read in metadata for the file. See the API documentation for more details. -## Database Examples ## +## Database Examples -### Anonymous IP Database ### +### Anonymous IP Database ```csharp @@ -341,7 +342,7 @@ using (var reader = new DatabaseReader("GeoIP2-Anonymous-IP.mmdb")) } ``` -### Anonymous Plus Database ### +### Anonymous Plus Database ```csharp @@ -361,7 +362,7 @@ using (var reader = new DatabaseReader("GeoIP-Anonymous-Plus.mmdb")) } ``` -### ASN ### +### ASN ```csharp @@ -374,7 +375,7 @@ using (var reader = new DatabaseReader("GeoLite2-ASN.mmdb")) } ``` -### City Database ### +### City Database ```csharp // This creates the DatabaseReader object, which should be reused across @@ -401,7 +402,7 @@ using (var reader = new DatabaseReader("GeoIP2-City.mmdb")) } ``` -### Connection-Type Database ### +### Connection-Type Database ```csharp @@ -413,7 +414,7 @@ using (var reader = new DatabaseReader("GeoIP2-Connection-Type.mmdb")) } ``` -### Domain Database ### +### Domain Database ```csharp @@ -425,7 +426,7 @@ using (var reader = new DatabaseReader("GeoIP2-Domain.mmdb")) } ``` -### Enterprise Database ### +### Enterprise Database ```csharp using (var reader = new DatabaseReader("/path/to/GeoIP2-Enterprise.mmdb")) @@ -459,7 +460,7 @@ using (var reader = new DatabaseReader("/path/to/GeoIP2-Enterprise.mmdb")) } ``` -### ISP Database ### +### ISP Database ```csharp @@ -474,20 +475,20 @@ using (var reader = new DatabaseReader("GeoIP2-ISP.mmdb")) } ``` -## Exceptions ## +## Exceptions -### Database ### +### Database If the database is corrupt or otherwise invalid, a `MaxMind.Db.InvalidDatabaseException` will be thrown. -If an address is not available in the database, a -`AddressNotFoundException` will be thrown. +If an address is not available in the database, a `AddressNotFoundException` +will be thrown. -### Web Service ### +### Web Service -For details on the possible errors returned by the web service itself, [see -the GeoIP2 web service documentation](https://dev.maxmind.com/geoip/docs/web-services?lang=en). +For details on the possible errors returned by the web service itself, +[see the GeoIP2 web service documentation](https://dev.maxmind.com/geoip/docs/web-services?lang=en). If the web service returns an explicit error document, this is thrown as a `AddressNotFoundException`, a `AuthenticationException`, a @@ -495,85 +496,83 @@ If the web service returns an explicit error document, this is thrown as a If some sort of transport error occurs, an `HttpException` is thrown. This is thrown when some sort of unanticipated error occurs, such as the web service -returning a 500 or an invalid error document. If the web service request -returns any status code besides 200, 4xx, or 5xx, this also becomes a -`HttpException`. +returning a 500 or an invalid error document. If the web service request returns +any status code besides 200, 4xx, or 5xx, this also becomes a `HttpException`. Finally, if the web service returns a 200 but the body is invalid, the client throws a `GeoIP2Exception`. This exception also is the parent exception to the above exceptions. -## Values to use for Database or Dictionary Keys ## +## Values to use for Database or Dictionary Keys -**We strongly discourage you from using a value from any `Names` property as -a key in a database or dictionary.** +**We strongly discourage you from using a value from any `Names` property as a +key in a database or dictionary.** These names may change between releases. Instead we recommend using one of the following: -* `MaxMind.GeoIP2.Model.City` - `City.GeoNameId` -* `MaxMind.GeoIP2.Model.Continent` - `Continent.Code` or `Continent.GeoNameId` -* `MaxMind.GeoIP2.Model.Country` and `MaxMind.GeoIP2.Model.RepresentedCountry` +- `MaxMind.GeoIP2.Model.City` - `City.GeoNameId` +- `MaxMind.GeoIP2.Model.Continent` - `Continent.Code` or `Continent.GeoNameId` +- `MaxMind.GeoIP2.Model.Country` and `MaxMind.GeoIP2.Model.RepresentedCountry` - `Country.IsoCode` or `Country.GeoNameId` -* `MaxMind.GeoIP2.Model.Subdivision` - `Subdivision.IsoCode` or +- `MaxMind.GeoIP2.Model.Subdivision` - `Subdivision.IsoCode` or `Subdivision.GeoNameId` -## Multi-Threaded Use ## +## Multi-Threaded Use This API fully supports use in multi-threaded applications. When using the -`DatabaseReader` in a multi-threaded application, we suggest creating one -object and sharing that among threads. +`DatabaseReader` in a multi-threaded application, we suggest creating one object +and sharing that among threads. -## What data is returned? ## +## What data is returned? -While many of the end points return the same basic records, the attributes -which can be populated vary between end points. In addition, while an end -point may offer a particular piece of data, MaxMind does not always have every -piece of data for any given IP address. +While many of the end points return the same basic records, the attributes which +can be populated vary between end points. In addition, while an end point may +offer a particular piece of data, MaxMind does not always have every piece of +data for any given IP address. Because of these factors, it is possible for any end point to return a record where some or all of the attributes are unpopulated. -See the [GeoIP2 web services -documentation](https://dev.maxmind.com/geoip/docs/web-services?lang=en) for -details on what data each end point may return. +See the +[GeoIP2 web services documentation](https://dev.maxmind.com/geoip/docs/web-services?lang=en) +for details on what data each end point may return. -The only piece of data which is always returned is the `ipAddress` attribute -in the `MaxMind.GeoIP2.Traits` record. +The only piece of data which is always returned is the `ipAddress` attribute in +the `MaxMind.GeoIP2.Traits` record. -## Integration with GeoNames ## +## Integration with GeoNames [GeoNames](https://www.geonames.org/) offers web services and downloadable databases with data on geographical features around the world, including populated places. They offer both free and paid premium data. Each feature is uniquely identified by a `geonameId`, which is an integer. -Many of the records returned by the GeoIP2 web services and databases include -a `geonameId` property. This is the ID of a geographical feature (city, -region, country, etc.) in the GeoNames database. +Many of the records returned by the GeoIP2 web services and databases include a +`geonameId` property. This is the ID of a geographical feature (city, region, +country, etc.) in the GeoNames database. -Some of the data that MaxMind provides is also sourced from GeoNames. We -source things like place names, ISO codes, and other similar data from the -GeoNames premium data set. +Some of the data that MaxMind provides is also sourced from GeoNames. We source +things like place names, ISO codes, and other similar data from the GeoNames +premium data set. -## Reporting data problems ## +## Reporting data problems -If the problem you find is that an IP address is incorrectly mapped, -please +If the problem you find is that an IP address is incorrectly mapped, please [submit your correction to MaxMind](https://www.maxmind.com/en/correction). -If you find some other sort of mistake, like an incorrect spelling, please -check the [GeoNames site](https://www.geonames.org/) first. Once you've -searched for a place and found it on the GeoNames map view, there are a number -of links you can use to correct data ("move", "edit", "alternate names", -etc.). Once the correction is part of the GeoNames data set, it will be -automatically incorporated into future MaxMind releases. +If you find some other sort of mistake, like an incorrect spelling, please check +the [GeoNames site](https://www.geonames.org/) first. Once you've searched for a +place and found it on the GeoNames map view, there are a number of links you can +use to correct data ("move", "edit", "alternate names", etc.). Once the +correction is part of the GeoNames data set, it will be automatically +incorporated into future MaxMind releases. If you are a paying MaxMind customer and you're not sure where to submit a -correction, please [contact MaxMind -support](https://www.maxmind.com/en/support) for help. +correction, please [contact MaxMind support](https://www.maxmind.com/en/support) +for help. -## Other Support ## +## Other Support Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/GeoIP2-dotnet/issues). @@ -581,22 +580,22 @@ Please report all issues with this code using the If you are having an issue with a MaxMind service that is not specific to the client API, please see [our support page](https://www.maxmind.com/en/support). -## Contributing ## +## Contributing Patches and pull requests are encouraged. Please include unit tests whenever possible. -## Versioning ## +## Versioning The API uses [Semantic Versioning](https://semver.org/). However, adding new methods to `IGeoIP2Provider`, `IGeoIP2DatabaseReader`, and `IGeoIP2WebServicesClient` will not be considered a breaking change. These -interfaces only exist to facilitate unit testing and dependency injection. -They will be updated to match additional methods added to the `DatabaseReader` -and `WebServiceClient`. Such additions will be accompanied by a minor version -bump (e.g., 1.2.x to 1.3.0). +interfaces only exist to facilitate unit testing and dependency injection. They +will be updated to match additional methods added to the `DatabaseReader` and +`WebServiceClient`. Such additions will be accompanied by a minor version bump +(e.g., 1.2.x to 1.3.0). -## Copyright and License ## +## Copyright and License This software is Copyright (c) 2013-2025 by MaxMind, Inc. diff --git a/releasenotes.md b/releasenotes.md index d27f3dd0..381de461 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,453 +1,401 @@ -GeoIP2 .NET API Release Notes -============================= - -6.0.0 ------------------- - -* **BREAKING:** All model and response classes have been converted from - classes to C# records. -* **BREAKING:** Constructor-based deserialization replaced with - property-based initialization using `[MapKey]` attributes. The - `[Constructor]` and `[Parameter]` attributes are no longer used on - GeoIP2 model classes. -* **BREAKING:** `SetLocales()` replaced with `WithLocales()` which returns - a new instance via record `with` expressions instead of mutating in - place. -* **BREAKING:** Properties now use `init` setters instead of - `internal set`. -* **BREAKING:** The `Locales` property on `NamedEntity` (and all subclasses) - is now `public` (previously `protected internal`). This is required for - the record `with` expression pattern. -* **BREAKING:** Code that constructs model objects using constructor - parameters (e.g., `new Traits(domain: "example.com")`) must switch to - object initializer syntax (e.g., `new Traits { Domain = "example.com" }`). -* **BREAKING:** The custom `ToString()` on `NamedEntity` (and all - subclasses: City, Country, Continent, Subdivision, RepresentedCountry) - has been removed. Records provide a compiler-generated `ToString()` that - outputs all property values. Use the `Name` property directly instead. -* Added `InternalsVisibleTo` for `MaxMind.MinFraud` assembly. - -5.5.0 ------------------- - -* `AnonymousPlus` and `TryAnonymousPlus` methods have been added to - `IGeoIP2DatabaseReader`. These methods were previously only available - on `DatabaseReader`. -* The `TryXxx` methods on `DatabaseReader` and `IGeoIP2DatabaseReader` now - use the `[MaybeNullWhen(false)]` attribute instead of nullable `out` - parameters. This enables the compiler to understand that the `out` - parameter is non-null when the method returns `true`, eliminating the - need for redundant null checks or null-forgiving operators after a - successful lookup. Pull request by Dmitry Solovev. GitHub #387. - -5.4.1 (2025-11-24) ------------------- - -* First release via Trusted Publishing. - -5.4.0 (2025-11-20) ------------------- - -* .NET 10.0 has been added as a target. -* A new `Anonymizer` object has been added to `InsightsResponse`. This object +# GeoIP2 .NET API Release Notes + +## 6.0.0 + +- **BREAKING:** All model and response classes have been converted from classes + to C# records. +- **BREAKING:** Constructor-based deserialization replaced with property-based + initialization using `[MapKey]` attributes. The `[Constructor]` and + `[Parameter]` attributes are no longer used on GeoIP2 model classes. +- **BREAKING:** `SetLocales()` replaced with `WithLocales()` which returns a new + instance via record `with` expressions instead of mutating in place. +- **BREAKING:** Properties now use `init` setters instead of `internal set`. +- **BREAKING:** The `Locales` property on `NamedEntity` (and all subclasses) is + now `public` (previously `protected internal`). This is required for the + record `with` expression pattern. +- **BREAKING:** Code that constructs model objects using constructor parameters + (e.g., `new Traits(domain: "example.com")`) must switch to object initializer + syntax (e.g., `new Traits { Domain = "example.com" }`). +- **BREAKING:** The custom `ToString()` on `NamedEntity` (and all subclasses: + City, Country, Continent, Subdivision, RepresentedCountry) has been removed. + Records provide a compiler-generated `ToString()` that outputs all property + values. Use the `Name` property directly instead. +- Added `InternalsVisibleTo` for `MaxMind.MinFraud` assembly. + +## 5.5.0 + +- `AnonymousPlus` and `TryAnonymousPlus` methods have been added to + `IGeoIP2DatabaseReader`. These methods were previously only available on + `DatabaseReader`. +- The `TryXxx` methods on `DatabaseReader` and `IGeoIP2DatabaseReader` now use + the `[MaybeNullWhen(false)]` attribute instead of nullable `out` parameters. + This enables the compiler to understand that the `out` parameter is non-null + when the method returns `true`, eliminating the need for redundant null checks + or null-forgiving operators after a successful lookup. Pull request by Dmitry + Solovev. GitHub #387. + +## 5.4.1 (2025-11-24) + +- First release via Trusted Publishing. + +## 5.4.0 (2025-11-20) + +- .NET 10.0 has been added as a target. +- A new `Anonymizer` object has been added to `InsightsResponse`. This object provides anonymizer-related data including VPN confidence scoring, provider name detection, and network last seen date. This data is available from the GeoIP2 Insights web service. -* A new `IpRiskSnapshot` property has been added to `MaxMind.GeoIP2.Model.Traits`. - This provides a risk score associated with the IP address, ranging from 0.01 - to 99. Higher scores indicate greater risk. This is available from the GeoIP2 - Insights web service. -* The following properties in `MaxMind.GeoIP2.Model.Traits` have been marked +- A new `IpRiskSnapshot` property has been added to + `MaxMind.GeoIP2.Model.Traits`. This provides a risk score associated with the + IP address, ranging from 0.01 to 99. Higher scores indicate greater risk. This + is available from the GeoIP2 Insights web service. +- The following properties in `MaxMind.GeoIP2.Model.Traits` have been marked `Obsolete` and users should migrate to using the `Anonymizer` object on the response instead: `IsAnonymous`, `IsAnonymousVpn`, `IsHostingProvider`, `IsPublicProxy`, `IsResidentialProxy`, and `IsTorExitNode`. These properties will continue to work but are deprecated in favor of the new `Anonymizer` object. -* A new `MaxMind.GeoIP2.Model.Anonymizer` class has been added with the +- A new `MaxMind.GeoIP2.Model.Anonymizer` class has been added with the following properties: - * `Confidence` - A score ranging from 1 to 99 representing percent confidence + - `Confidence` - A score ranging from 1 to 99 representing percent confidence that the network is currently part of an actively used VPN service. - * `IsAnonymous` - Indicates whether the IP belongs to any sort of anonymous + - `IsAnonymous` - Indicates whether the IP belongs to any sort of anonymous network. - * `IsAnonymousVpn` - True if the IP is registered to an anonymous VPN provider. - * `IsHostingProvider` - True if the IP belongs to a hosting or VPN provider. - * `IsPublicProxy` - True if the IP belongs to a public proxy. - * `IsResidentialProxy` - True if the IP is on a suspected anonymizing network + - `IsAnonymousVpn` - True if the IP is registered to an anonymous VPN + provider. + - `IsHostingProvider` - True if the IP belongs to a hosting or VPN provider. + - `IsPublicProxy` - True if the IP belongs to a public proxy. + - `IsResidentialProxy` - True if the IP is on a suspected anonymizing network and belongs to a residential ISP. - * `IsTorExitNode` - True if the IP belongs to a Tor exit node. - * `NetworkLastSeen` - The last day the network was sighted in analysis of + - `IsTorExitNode` - True if the IP belongs to a Tor exit node. + - `NetworkLastSeen` - The last day the network was sighted in analysis of anonymized networks (available on .NET 6.0+ as `DateOnly`). - * `ProviderName` - The name of the VPN provider associated with the network. + - `ProviderName` - The name of the VPN provider associated with the network. -5.3.0 (2025-05-05) ------------------- +## 5.3.0 (2025-05-05) -* Support for the GeoIP Anonymous Plus database has been added. To do a - lookup in this database, use the `AnonymousPlus` And `TryAnonymousPlus` - methods on `DatabaseReader`. -* .NET 6.0 and .NET 7.0 have been removed as targets as they have both - reach their end of support from Microsoft. If you are using these versions, - the .NET Standard 2.1 target should continue working for you. -* .NET 9.0 has been added as a target. -* `MetroCode` in `MaxMind.GeoIP2.Model.Location` has been marked `Obsolete`. - The code values are no longer being maintained. +- Support for the GeoIP Anonymous Plus database has been added. To do a lookup + in this database, use the `AnonymousPlus` And `TryAnonymousPlus` methods on + `DatabaseReader`. +- .NET 6.0 and .NET 7.0 have been removed as targets as they have both reach + their end of support from Microsoft. If you are using these versions, the .NET + Standard 2.1 target should continue working for you. +- .NET 9.0 has been added as a target. +- `MetroCode` in `MaxMind.GeoIP2.Model.Location` has been marked `Obsolete`. The + code values are no longer being maintained. -5.2.0 (2023-12-05) ------------------- +## 5.2.0 (2023-12-05) -* .NET 5.0 has been removed as a target as it has reach its end of life. +- .NET 5.0 has been removed as a target as it has reach its end of life. However, if you are using .NET 5.0, the .NET Standard 2.1 target should continue working for you. -* .NET 7.0 and .NET 8.0 have been added as a target. -* The `IsAnycast` property was added to `MaxMind.GeoIP2.Model.Traits`. This - returns `true` if the IP address belongs to an [anycast - network](https://en.wikipedia.org/wiki/Anycast). This is available for the - GeoIP2 Country, City Plus, and Insights web services and the GeoIP2 Country, - City, and Enterprise databases. +- .NET 7.0 and .NET 8.0 have been added as a target. +- The `IsAnycast` property was added to `MaxMind.GeoIP2.Model.Traits`. This + returns `true` if the IP address belongs to an + [anycast network](https://en.wikipedia.org/wiki/Anycast). This is available + for the GeoIP2 Country, City Plus, and Insights web services and the GeoIP2 + Country, City, and Enterprise databases. -5.1.0 (2022-02-04) ------------------- +## 5.1.0 (2022-02-04) -* Update System.Text.Json to 6.0.1 for .NET Standard 2.0 and 2.1. +- Update System.Text.Json to 6.0.1 for .NET Standard 2.0 and 2.1. -5.0.0 (2022-02-04) ------------------- +## 5.0.0 (2022-02-04) -* This library no longer targets .NET 4.6.1. -* .NET 6.0 was added as a target. -* On .NET 5.0+, HttpClient is now used for synchronous requests instead of +- This library no longer targets .NET 4.6.1. +- .NET 6.0 was added as a target. +- On .NET 5.0+, HttpClient is now used for synchronous requests instead of WebRequest. -4.1.0 (2021-11-19) ------------------- +## 4.1.0 (2021-11-19) -* Support for mobile country code (MCC) and mobile network codes (MNC) was - added for the GeoIP2 ISP and Enterprise databases as well as the GeoIP2 - City and Insights web services. The `MobileCountryCode` and - `MobileNetworkCode` properties were added to `MaxMind.GeoIP2.Responses.IspResponse` - for the GeoIP2 ISP database and `MaxMind.GeoIP2.Model.Traits` for the - Enterprise database and the GeoIP2 City and Insights web services. We expect - this data to be available by late January, 2022. +- Support for mobile country code (MCC) and mobile network codes (MNC) was added + for the GeoIP2 ISP and Enterprise databases as well as the GeoIP2 City and + Insights web services. The `MobileCountryCode` and `MobileNetworkCode` + properties were added to `MaxMind.GeoIP2.Responses.IspResponse` for the GeoIP2 + ISP database and `MaxMind.GeoIP2.Model.Traits` for the Enterprise database and + the GeoIP2 City and Insights web services. We expect this data to be available + by late January, 2022. -4.0.1 (2020-11-19) ------------------- +## 4.0.1 (2020-11-19) -* This release fixes an issue with 4.0.0 where the synchronous web service +- This release fixes an issue with 4.0.0 where the synchronous web service methods could cause an unexpected JSON decoding error. There are no other changes. The async `WebServiceClient` methods and the `DatabaseReader` were not affected by the issue. -4.0.0 (2020-11-17) ------------------- +## 4.0.0 (2020-11-17) -* This library now requires .NET Framework 4.6.1 or greater or .NET Standard - 2.0 or greater. -* .NET 5.0 was added as a target framework. -* `System.Text.Json` is now used for deserialization of web service requests. - `Newtonsoft.Json` is no longer supported for serialization or - deserialization. -* The `Names` properties on `NamedEntity` models are now +- This library now requires .NET Framework 4.6.1 or greater or .NET Standard 2.0 + or greater. +- .NET 5.0 was added as a target framework. +- `System.Text.Json` is now used for deserialization of web service requests. + `Newtonsoft.Json` is no longer supported for serialization or deserialization. +- The `Names` properties on `NamedEntity` models are now `IReadOnlyDictionary`. -* The `Subdivisions` property on `CityResponse` and `InsightsResponse` is now - an `IReadOnlyList`. -* `GeoNameId` properties on `NamedEntity` models are now `long?` rather than +- The `Subdivisions` property on `CityResponse` and `InsightsResponse` is now an + `IReadOnlyList`. +- `GeoNameId` properties on `NamedEntity` models are now `long?` rather than `int?` to match the underlying database. -* The `httpMessageHandler` argument is now correctly initialized by the +- The `httpMessageHandler` argument is now correctly initialized by the `WebServiceClient` constructor. -* The `Metadata` property was added to `IGeoIP2DatabaseReader`. Pull request - by Mihai Valentin Caracostea. GitHub #134 & #135. +- The `Metadata` property was added to `IGeoIP2DatabaseReader`. Pull request by + Mihai Valentin Caracostea. GitHub #134 & #135. -3.3.0 (2020-09-25) ------------------- +## 3.3.0 (2020-09-25) -* The `IsResidentialProxy` property has been added to +- The `IsResidentialProxy` property has been added to `MaxMind.GeoIP2.Responses.AnonymousIPResponse` and `MaxMind.GeoIP2.Model.Traits`. -3.2.0 (2020-04-28) ------------------- +## 3.2.0 (2020-04-28) -* You may now create `WebServiceClient` as Typed Client with - `IHttpClientFactory` in .NET Core 2.1+. Pull Request by Bojan Nikolić. - GitHub #115 & #117. -* The `WebServiceClient` constructor now supports an optional - `httpMessageHandler` parameter. This is used in creating the `HttpClient` - for asynchronous requests. +- You may now create `WebServiceClient` as Typed Client with + `IHttpClientFactory` in .NET Core 2.1+. Pull Request by Bojan Nikolić. GitHub + #115 & #117. +- The `WebServiceClient` constructor now supports an optional + `httpMessageHandler` parameter. This is used in creating the `HttpClient` for + asynchronous requests. -3.1.0 (2019-12-06) ------------------- +## 3.1.0 (2019-12-06) -* This library has been updated to support the nullable reference types +- This library has been updated to support the nullable reference types introduced in C# 8.0. -* A `Network` property has been added to the various response models. This - represents the largest network where all the fields besides the IP - address are the same. -* The `StaticIPScore` property has been added to `MaxMind.GeoIP2.Model.Traits`. - This output is available from GeoIP2 Precision Insights. It is an indicator - of how static or dynamic an IP address is. -* The `UserCount` property has been added to `MaxMind.GeoIP2.Model.Traits`. - This output is available from GeoIP2 Precision Insights. It is an - estimate of the number of users sharing the IP/network over the past - 24 hours. -* Updated documentation of anonymizer properties - `IsAnonymousVpn` and +- A `Network` property has been added to the various response models. This + represents the largest network where all the fields besides the IP address are + the same. +- The `StaticIPScore` property has been added to `MaxMind.GeoIP2.Model.Traits`. + This output is available from GeoIP2 Precision Insights. It is an indicator of + how static or dynamic an IP address is. +- The `UserCount` property has been added to `MaxMind.GeoIP2.Model.Traits`. This + output is available from GeoIP2 Precision Insights. It is an estimate of the + number of users sharing the IP/network over the past 24 hours. +- Updated documentation of anonymizer properties - `IsAnonymousVpn` and `IsHostingProvider` - to be more descriptive. -* `netstandard2.1` was added as a target framework. +- `netstandard2.1` was added as a target framework. -3.0.0 (2018-04-11) ------------------- +## 3.0.0 (2018-04-11) -* The `userId` constructor parameter for `WebServiceClient` was renamed to +- The `userId` constructor parameter for `WebServiceClient` was renamed to `accountId` and support was added for the error codes `ACCOUNT_ID_REQUIRED` and `ACCOUNT_ID_UNKNOWN`. -* The exception classes are no longer serializable when using the .NET - Framework. This eliminates a difference between the .NET Framework - assemblies and the .NET Standard ones. -* The `AutonomousSystemNumber` properties on `MaxMind.GeoIP2.Model.Traits`, +- The exception classes are no longer serializable when using the .NET + Framework. This eliminates a difference between the .NET Framework assemblies + and the .NET Standard ones. +- The `AutonomousSystemNumber` properties on `MaxMind.GeoIP2.Model.Traits`, `MaxMind.GeoIP2.Responses.AsnResponse`, and `MaxMind.GeoIP2.Responses.IspResponse` are now `long?` to match the underlying types in the databases. -* `MaxMind.Db` was upgraded to 2.4.0. This adds a new file mode enum value for +- `MaxMind.Db` was upgraded to 2.4.0. This adds a new file mode enum value for the database reader, `FileAccessMode.MemoryMappedGlobal`. When used, this will open the file in global memory map mode. This requires the "create global objects" right. -2.10.0 (2018-01-19) -------------------- +## 2.10.0 (2018-01-19) -* The `IsInEuropeanUnion` property was added to `MaxMind.GeoIP2.Model.Country` +- The `IsInEuropeanUnion` property was added to `MaxMind.GeoIP2.Model.Country` and `MaxMind.GeoIP2.Model.RepresentedCountry`. This property is `true` if the country is a member state of the European Union. -2.9.0 (2017-10-27) ------------------- +## 2.9.0 (2017-10-27) -* The following new anonymizer properties were added to +- The following new anonymizer properties were added to `MaxMind.GeoIP2.Model.Traits` for use with GeoIP2 Precision Insights: - `IsAnonymous`, `IsAnonymousVpn`, `IsHostingProvider`, `IsPublicProxy`, - and `IsTorExitNode`. -* Deserialization of the registered country when reading a GeoLite2 Country or + `IsAnonymous`, `IsAnonymousVpn`, `IsHostingProvider`, `IsPublicProxy`, and + `IsTorExitNode`. +- Deserialization of the registered country when reading a GeoLite2 Country or GeoIP2 Country database now works. Previously, it was deserialized to the wrong name. Reported by oliverherdener. -* A `netstandard2.0` target was added to eliminate additional dependencies - required by the `netstandard1.4` target. Pull request by Adeel Mujahid. - GitHub #81. -* As part of the above work, the separate Mono build files were dropped. As - of Mono 5.0.0, `msbuild` is supported. +- A `netstandard2.0` target was added to eliminate additional dependencies + required by the `netstandard1.4` target. Pull request by Adeel Mujahid. GitHub + #81. +- As part of the above work, the separate Mono build files were dropped. As of + Mono 5.0.0, `msbuild` is supported. -2.8.0 (2017-05-08) ------------------- +## 2.8.0 (2017-05-08) -* Add support for GeoLite2 ASN. -* Switch to the updated MSBuild .NET Core build system. -* Move tests from NUnit to xUnit.net. -* Upgrade to `MaxMind.Db` 2.2.0. +- Add support for GeoLite2 ASN. +- Switch to the updated MSBuild .NET Core build system. +- Move tests from NUnit to xUnit.net. +- Upgrade to `MaxMind.Db` 2.2.0. -2.7.2 (2016-11-22) ------------------- +## 2.7.2 (2016-11-22) -* Use framework assembly for `System.Net.Http` on .NET 4.5. -* Update for .NET Core 1.1. +- Use framework assembly for `System.Net.Http` on .NET 4.5. +- Update for .NET Core 1.1. -2.7.1 (2016-08-08) ------------------- +## 2.7.1 (2016-08-08) -* Re-release of 2.7.0 to fix strong name issue. No code changes. +- Re-release of 2.7.0 to fix strong name issue. No code changes. -2.7.0 (2016-08-01) ------------------- +## 2.7.0 (2016-08-01) -* First non-beta release with .NET Core support. -* The tests now use the .NET Core NUnit runner. Pull request by Adeel Mujahid. +- First non-beta release with .NET Core support. +- The tests now use the .NET Core NUnit runner. Pull request by Adeel Mujahid. GitHub #68. -* Updated documentation to clarify what the accuracy radius refers to. +- Updated documentation to clarify what the accuracy radius refers to. -2.7.0-beta2 (2016-06-02) ------------------------- +## 2.7.0-beta2 (2016-06-02) -* Added handling of additional error codes that the web service may return. -* Update for .NET Core RC2. Pull request by Adeel Mujahid. GitHub #64. +- Added handling of additional error codes that the web service may return. +- Update for .NET Core RC2. Pull request by Adeel Mujahid. GitHub #64. -2.7.0-beta1 (2016-05-15) ------------------------- +## 2.7.0-beta1 (2016-05-15) -* .NET Core support. Switched to `dotnet/cli` for building. Pull request by +- .NET Core support. Switched to `dotnet/cli` for building. Pull request by Adeel Mujahid. GitHub #60. -* Updated documentation to reflect that the accuracy radius is now included - in City. +- Updated documentation to reflect that the accuracy radius is now included in + City. -2.6.0 (2016-04-15) ------------------- +## 2.6.0 (2016-04-15) -* Added support for the GeoIP2 Enterprise database. +- Added support for the GeoIP2 Enterprise database. -2.6.0-beta3 (2016-02-10) ------------------------- +## 2.6.0-beta3 (2016-02-10) -* Try-based lookup methods were added to `DatabaseReader` as an alternative to +- Try-based lookup methods were added to `DatabaseReader` as an alternative to the existing methods. These methods return a boolean indicating whether the - record was found rather than throwing an exception when it is not found. - Pull request by Mani Gandham. GitHub #31, #50. + record was found rather than throwing an exception when it is not found. Pull + request by Mani Gandham. GitHub #31, #50. -2.6.0-beta2 (2016-01-18) ------------------------- +## 2.6.0-beta2 (2016-01-18) -* Parameterless endpoint methods were added to `WebServiceClient`. These - return the record for the requesting IP address using the `me` endpoint as - documented in the web services API documentation. -* The target framework is now .NET 4.5 rather than 4.5.2 in order to work - better with Mono. GitHub #44. +- Parameterless endpoint methods were added to `WebServiceClient`. These return + the record for the requesting IP address using the `me` endpoint as documented + in the web services API documentation. +- The target framework is now .NET 4.5 rather than 4.5.2 in order to work better + with Mono. GitHub #44. -2.6.0-beta1 (2016-01-18) ------------------------- +## 2.6.0-beta1 (2016-01-18) -* Upgrade MaxMindb.Db reader to 2.0.0-beta1. This includes significant +- Upgrade MaxMindb.Db reader to 2.0.0-beta1. This includes significant performance increases. -2.5.0 (2015-12-04) ------------------- +## 2.5.0 (2015-12-04) -* IMPORTANT: The target framework is now 4.5.2. Microsoft is ending support - for 4.0, 4.5, and 4.5.1 on January 12, 2016. Removing support for these - frameworks allows us to remove the dependency on the BCL libraries and fixes - several outstanding issues. Closes #38, #39, #40, and #42. -* The assembly version was bumped to 2.5.0. -* Classes subclassing `NamedEntity` now have a default locale of `en`. This +- IMPORTANT: The target framework is now 4.5.2. Microsoft is ending support for + 4.0, 4.5, and 4.5.1 on January 12, 2016. Removing support for these frameworks + allows us to remove the dependency on the BCL libraries and fixes several + outstanding issues. Closes #38, #39, #40, and #42. +- The assembly version was bumped to 2.5.0. +- Classes subclassing `NamedEntity` now have a default locale of `en`. This allows the `Name` property to be used (for English names) when the object is deserialized from JSON. Closes #41. -* The `locale` parameter for the `DatabaseReader` and `WebServiceClient` +- The `locale` parameter for the `DatabaseReader` and `WebServiceClient` constructors is now an `IEnumerable` rather than a `List`. -* The tests now use NUnit 3. +- The tests now use NUnit 3. -2.4.0 (2015-09-23) ------------------- +## 2.4.0 (2015-09-23) -* Updated MaxMind.Db to 1.2.0. +- Updated MaxMind.Db to 1.2.0. -2.4.0-beta1 (2015-09-10) ------------------------- +## 2.4.0-beta1 (2015-09-10) -* Async support was added to the `WebServiceClient`. Each web-service end - point now has a corresponding `*Async(ip)` method. GitHub #1. -* Use of RestSharp was replaced by `HttpWebRequest` for synchronous HTTP +- Async support was added to the `WebServiceClient`. Each web-service end point + now has a corresponding `*Async(ip)` method. GitHub #1. +- Use of RestSharp was replaced by `HttpWebRequest` for synchronous HTTP requests and `HttpClient` for asynchronous requests. Microsoft BCL libraries and `System.Net.Http` are used to provide `async`/`await` and `HttpClient` support on .NET 4.0. GitHub #33. -* The library now has a strong name. +- The library now has a strong name. -2.3.1 (2015-07-21) ------------------- +## 2.3.1 (2015-07-21) -* Upgrade to MaxMind.Db 1.1.0. -* Fix serialization on exceptions. +- Upgrade to MaxMind.Db 1.1.0. +- Fix serialization on exceptions. -2.3.1-beta1 (2015-06-30) ------------------------- +## 2.3.1-beta1 (2015-06-30) -* Upgrade to Json.NET 7.0.1. -* Upgrade to MaxMind.Db 1.1.0-beta1. This release includes a number of +- Upgrade to Json.NET 7.0.1. +- Upgrade to MaxMind.Db 1.1.0-beta1. This release includes a number of significant improvements for the memory-mapped file mode. -2.3.0 (2015-06-29) ------------------- - -* `AverageIncome` and `PopulationDensity` were added to the `Location` - model for use with the new fields in GeoIP2 Insights. -* `IsAnonymousProxy` and `IsSatelliteProvider` in `MaxMind.GeoIP2.Model.Traits` - have been deprecated. Please use our [GeoIP2 Anonymous IP - database](https://www.maxmind.com/en/geoip2-anonymous-ip-database) to - determine whether an IP address is used by an anonymizing service. - -2.2.0 (2015-05-19) ------------------- - -* All of the database methods in `DatabaseReader` and all of the web service - methods in `WebServiceClient` now have a counterpart that takes an - `IPAddress` instead of a `string`. Pull request by Guillaume Turri. GitHub - #24. -* The `JsonIgnore` attribute was added to `Names` in `NamedEntity` and - `Subdivisions` in `AbstractCityResponse` as these were already exposed to +## 2.3.0 (2015-06-29) + +- `AverageIncome` and `PopulationDensity` were added to the `Location` model for + use with the new fields in GeoIP2 Insights. +- `IsAnonymousProxy` and `IsSatelliteProvider` in `MaxMind.GeoIP2.Model.Traits` + have been deprecated. Please use our + [GeoIP2 Anonymous IP database](https://www.maxmind.com/en/geoip2-anonymous-ip-database) + to determine whether an IP address is used by an anonymizing service. + +## 2.2.0 (2015-05-19) + +- All of the database methods in `DatabaseReader` and all of the web service + methods in `WebServiceClient` now have a counterpart that takes an `IPAddress` + instead of a `string`. Pull request by Guillaume Turri. GitHub #24. +- The `JsonIgnore` attribute was added to `Names` in `NamedEntity` and + `Subdivisions` in `AbstractCityResponse` as these were already exposed to JSON.NET through the private field backing them. Pull request by Dan Byrne GitHub #21. -* The interfaces `IGeoIP2DatabaseReader` and `IGeoIP2WebServicesClient` were +- The interfaces `IGeoIP2DatabaseReader` and `IGeoIP2WebServicesClient` were added to facilitate dependency injection and mocking. Pull request by Naz Soogund. GitHub #22. -* A `HasCoordinates` getter was added to the `Location` class. This will - return true if both the `Latitude` and `Longitude` have values. Pull request - by Darren Hickling. GitHub #23. -* All of the response and model properties now set the appropriate +- A `HasCoordinates` getter was added to the `Location` class. This will return + true if both the `Latitude` and `Longitude` have values. Pull request by + Darren Hickling. GitHub #23. +- All of the response and model properties now set the appropriate `JsonProperty` rather than relying an JSON.NET's automatic matching. Serializing these objects should now product JSON much more similar to the JSON returned by the web service and internal structure of the data in database files. -* Dependencies were updated to most recent versions. +- Dependencies were updated to most recent versions. -2.1.0 (2014-11-06) ------------------- +## 2.1.0 (2014-11-06) -* Added support for the GeoIP2 Anonymous IP database. The `DatabaseReader` - class now has an `AnonymousIP()` method which returns an - `AnonymousIPResponse` object. +- Added support for the GeoIP2 Anonymous IP database. The `DatabaseReader` class + now has an `AnonymousIP()` method which returns an `AnonymousIPResponse` + object. -2.0.0 (2014-09-29) ------------------- +## 2.0.0 (2014-09-29) -* First production release. +- First production release. -0.5.0 (2014-09-24) ------------------- +## 0.5.0 (2014-09-24) -* The deprecated `CityIspOrg` and `Omni` methods were removed. -* `DatabaseReader` methods will now throw an `InvalidOperationException` when +- The deprecated `CityIspOrg` and `Omni` methods were removed. +- `DatabaseReader` methods will now throw an `InvalidOperationException` when called for the wrong database type. -* `DatabaseReader` now has a `Metadata` property that provides an object - containing the metadata for the open database. +- `DatabaseReader` now has a `Metadata` property that provides an object + containing the metadata for the open database. -0.4.0 (2014-07-22) ------------------- +## 0.4.0 (2014-07-22) -* The web service client API has been updated for the v2.1 release of the web +- The web service client API has been updated for the v2.1 release of the web service. In particular, the `CityIspOrg` and `Omni` methods on - `WebServiceClient` have been deprecated. The `City` method now provides all - of the data formerly provided by `CityIspOrg`, and the `Omni` method has - been replaced by the `Insights` method. -* Support was added for the GeoIP2 Connection Type, Domain, and ISP databases. - + `WebServiceClient` have been deprecated. The `City` method now provides all of + the data formerly provided by `CityIspOrg`, and the `Omni` method has been + replaced by the `Insights` method. +- Support was added for the GeoIP2 Connection Type, Domain, and ISP databases. -0.3.3 (2014-06-02) ------------------- +## 0.3.3 (2014-06-02) -* Constructors with named parameters were added to the model and response +- Constructors with named parameters were added to the model and response classes. (Jon Wynveen) -0.3.2 (2014-04-09) ------------------- +## 0.3.2 (2014-04-09) -* A constructor taking a `Stream` was added to `DatabaseReader`. -* Fixed dependency on wrong version of `Newtonsoft.Json`. +- A constructor taking a `Stream` was added to `DatabaseReader`. +- Fixed dependency on wrong version of `Newtonsoft.Json`. -0.3.1 (2014-02-13) ------------------- +## 0.3.1 (2014-02-13) -* Fixed broken error handling. Previously the wrong exceptions and error +- Fixed broken error handling. Previously the wrong exceptions and error messages were returned for some web service errors. -0.3.0 (2013-11-15) ------------------- +## 0.3.0 (2013-11-15) -* API CHANGE: Renamed exceptions to remove GeoIP2 prefixes. -* Improved error messages when RestSharp does not return an HTTP status code. +- API CHANGE: Renamed exceptions to remove GeoIP2 prefixes. +- Improved error messages when RestSharp does not return an HTTP status code. -0.2.0 (2013-10-25) ------------------- +## 0.2.0 (2013-10-25) -* First release with GeoIP2 database support. See `DatabaseReader` class. +- First release with GeoIP2 database support. See `DatabaseReader` class. -0.1.1 (2013-10-04) ------------------- +## 0.1.1 (2013-10-04) -* Build documentation. +- Build documentation. -0.1.0 (2013-09-20) ------------------- +## 0.1.0 (2013-09-20) -* Initial release. +- Initial release. From 805628deee725fd1cd78c0fc5fcafdcae8bb4ec2 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 08:00:43 -0700 Subject: [PATCH 05/15] Update release script for ATX headings Co-Authored-By: Claude Opus 4.6 --- dev-bin/release.sh | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dev-bin/release.sh b/dev-bin/release.sh index 3c0dbf3c..e7077851 100755 --- a/dev-bin/release.sh +++ b/dev-bin/release.sh @@ -52,8 +52,12 @@ fi changelog=$(cat releasenotes.md) -# GeoIP2-dotnet format: "5.4.0 (2025-11-20)" followed by "---" -regex='([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\)' +regex=' +## ([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\) + +((.| +)*) +' if [[ ! $changelog =~ $regex ]]; then echo "Could not find version/date in releasenotes.md!" @@ -62,14 +66,7 @@ fi version="${BASH_REMATCH[1]}" date="${BASH_REMATCH[3]}" - -# Extract release notes: everything after "---" line until next version header -notes=$(awk -v ver="$version" ' - $0 ~ "^" ver " \\(" { found=1; next } - found && /^-+$/ { in_notes=1; next } - in_notes && /^[0-9]+\.[0-9]+\.[0-9]+.* \([0-9]{4}-[0-9]{2}-[0-9]{2}\)/ { exit } - in_notes { print } -' releasenotes.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}') +notes="$(echo "${BASH_REMATCH[4]}" | sed -n -e '/^## [0-9]\+\.[0-9]\+\.[0-9]\+/,$!p')" if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then echo "$date is not today!" From 27c23e3aeaa36c81f5ab3f170078af2f11e68d1e Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 08:01:04 -0700 Subject: [PATCH 06/15] Add build.binlog and BenchmarkDotNet.Artifacts to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 31af1220..5c432671 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ ipch/ # ReSharper is a .NET coding add-in _ReSharper* +*.DotSettings # NCrunch *.ncrunch* @@ -103,6 +104,12 @@ AppPackages/ .idea *.iml +# Build logs +*.binlog + +# BenchmarkDotNet +BenchmarkDotNet.Artifacts/ + # Claude .claude @@ -126,3 +133,6 @@ Generated_Code #added for RIA/Silverlight projects _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML + +# Worktrees +.worktrees From 019947b7eaadd66bd7c5e91ea07eb84d350ceb62 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 08:01:23 -0700 Subject: [PATCH 07/15] Add 10.0.x to CodeQL analysis Co-Authored-By: Claude Opus 4.6 --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 423629e2..ed717f63 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,6 +35,7 @@ jobs: dotnet-version: | 8.0.x 9.0.x + 10.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 8e94ab793b7883c200659fedd0d198f420b08d38 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 15:31:44 +0000 Subject: [PATCH 08/15] Add WithLocales integration test for web service path Verify that Name properties are correctly populated after the WithLocales round-trip through the web service client, exercising the full chain of JSON deserialization, locale propagation via record with expressions, and Name resolution from the Names dictionary. Co-Authored-By: Claude Opus 4.6 --- MaxMind.GeoIP2.UnitTests/ResponseHelper.cs | 12 +++---- .../WebServiceClientTests.cs | 35 +++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/MaxMind.GeoIP2.UnitTests/ResponseHelper.cs b/MaxMind.GeoIP2.UnitTests/ResponseHelper.cs index 7f19018a..2a7c543d 100644 --- a/MaxMind.GeoIP2.UnitTests/ResponseHelper.cs +++ b/MaxMind.GeoIP2.UnitTests/ResponseHelper.cs @@ -7,18 +7,18 @@ internal static class ResponseHelper "city": { "confidence": 76, "geoname_id": 9876, - "names": {"en": "Minneapolis"} + "names": {"en": "Minneapolis", "ja": "\u30df\u30cd\u30a2\u30dd\u30ea\u30b9"} }, "continent": { "code": "NA", "geoname_id": 42, - "names": {"en": "North America"} + "names": {"en": "North America", "ja": "\u5317\u30a2\u30e1\u30ea\u30ab"} }, "country": { "confidence": 99, "iso_code": "US", "geoname_id": 1, - "names": {"en": "United States of America"} + "names": {"en": "United States of America", "ja": "\u30a2\u30e1\u30ea\u30ab"} }, "location": { "accuracy_radius": 1500, @@ -37,13 +37,13 @@ internal static class ResponseHelper "geoname_id": 2, "is_in_european_union": true, "iso_code": "DE", - "names": {"en": "Germany"} + "names": {"en": "Germany", "ja": "\u30c9\u30a4\u30c4"} }, "represented_country": { "geoname_id": 3, "is_in_european_union": true, "iso_code": "GB", - "names": {"en": "United Kingdom"}, + "names": {"en": "United Kingdom", "ja": "\u30a4\u30ae\u30ea\u30b9"}, "type": "military" }, "subdivisions": [ @@ -51,7 +51,7 @@ internal static class ResponseHelper "confidence": 88, "geoname_id": 574635, "iso_code": "MN", - "names": {"en": "Minnesota"} + "names": {"en": "Minnesota", "ja": "\u30df\u30cd\u30bd\u30bf"} }, {"iso_code": "TT"} ], diff --git a/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs b/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs index 72c25911..1aca9d3d 100644 --- a/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs +++ b/MaxMind.GeoIP2.UnitTests/WebServiceClientTests.cs @@ -58,7 +58,8 @@ public WebServiceClientTests() private bool _disposed; private WebServiceClient CreateClient(string type, string ipAddress = "1.2.3.4", - HttpStatusCode status = HttpStatusCode.OK, string? contentType = null, string content = "") + HttpStatusCode status = HttpStatusCode.OK, string? contentType = null, string content = "", + List? locales = null) { var service = type.Replace("Async", ""); @@ -80,7 +81,7 @@ private WebServiceClient CreateClient(string type, string ipAddress = "1.2.3.4", var host = _server.Urls[0].Replace("http://", ""); return new WebServiceClient(6, "0123456789", - locales: ["en"], + locales: locales ?? ["en"], host: host, timeout: TestTimeoutMilliseconds, disableHttps: true @@ -467,6 +468,36 @@ public void Constructors() Assert.NotNull(new WebServiceClient(accountId: id, licenseKey: key)); } + [Fact] + public void WithLocalesPopulatesNames() + { + var client = CreateClient("insights", content: InsightsJson); + var result = client.Insights("1.2.3.4"); + + Assert.Equal("Minneapolis", result.City.Name); + Assert.Equal("North America", result.Continent.Name); + Assert.Equal("United States of America", result.Country.Name); + Assert.Equal("Germany", result.RegisteredCountry.Name); + Assert.Equal("United Kingdom", result.RepresentedCountry.Name); + Assert.Equal("Minnesota", result.Subdivisions[0].Name); + } + + [Fact] + public void WithLocalesNonEnglish() + { + var client = CreateClient("insights", content: InsightsJson, + locales: ["ja"]); + var result = client.Insights("1.2.3.4"); + + Assert.IsType(result); + Assert.Equal("\u30df\u30cd\u30a2\u30dd\u30ea\u30b9", result.City.Name); + Assert.Equal("\u5317\u30a2\u30e1\u30ea\u30ab", result.Continent.Name); + Assert.Equal("\u30a2\u30e1\u30ea\u30ab", result.Country.Name); + Assert.Equal("\u30c9\u30a4\u30c4", result.RegisteredCountry.Name); + Assert.Equal("\u30a4\u30ae\u30ea\u30b9", result.RepresentedCountry.Name); + Assert.Equal("\u30df\u30cd\u30bd\u30bf", result.Subdivisions[0].Name); + } + #region NetCoreTests [Fact] From 7145062769033511ba56e044734372b79aa1cda2 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 15:32:51 +0000 Subject: [PATCH 09/15] Update CLAUDE.md for record-based patterns Rewrite documentation sections to reflect the class-to-record conversion: [MapKey] replaces [Parameter]/[Constructor], init replaces internal set, object initializers replace constructors, and backing field pattern replaces constructor date parsing. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 150 +++++++++++++++++++++--------------------------------- 1 file changed, 59 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 05338d30..6176a9fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,39 +46,33 @@ MaxMind.GeoIP2.UnitTests/ ### Key Design Patterns -#### 1. **Immutable Model Classes with Optional Parameters** +#### 1. **Immutable Model Records with Init Properties** -Model and response classes use C# classes (not records) with properties and -constructors that accept optional parameters with defaults: +Model and response classes use C# records with `init` properties: ```csharp -public class Traits +public record Traits { - public Traits( - [Parameter("autonomous_system_number")] long? autonomousSystemNumber = null, - [Parameter("autonomous_system_organization")] string? autonomousSystemOrganization = null, - // ... more parameters with defaults - ) - { - AutonomousSystemNumber = autonomousSystemNumber; - AutonomousSystemOrganization = autonomousSystemOrganization; - // ... - } - [JsonInclude] [JsonPropertyName("autonomous_system_number")] - public long? AutonomousSystemNumber { get; internal set; } + [MapKey("autonomous_system_number")] + public long? AutonomousSystemNumber { get; init; } + + [JsonInclude] + [JsonPropertyName("autonomous_system_organization")] + [MapKey("autonomous_system_organization")] + public string? AutonomousSystemOrganization { get; init; } } ``` **Key Points:** - Use `[JsonPropertyName]` for JSON field mapping (System.Text.Json) -- Use `[Parameter]` for MaxMind DB field mapping -- Use `[Constructor]` attribute for MaxMind DB deserialization -- Properties use `internal set` to prevent external modification while allowing - deserialization -- Default parameters in constructors avoid breaking changes +- Use `[MapKey("field_name")]` for MaxMind DB field mapping (replaces + `[Parameter]`) +- Properties use `init` setters for immutability +- No constructors needed — deserialization uses property initialization +- Records provide built-in equality, `with` expressions, and `ToString()` #### 2. **Conditional Compilation for .NET Version Differences** @@ -89,7 +83,7 @@ Some features are only available in newer .NET versions (e.g., `DateOnly` in #if NET6_0_OR_GREATER [JsonInclude] [JsonPropertyName("network_last_seen")] - public DateOnly? NetworkLastSeen { get; internal set; } + public DateOnly? NetworkLastSeen { get; init; } #endif ``` @@ -102,7 +96,7 @@ When deprecating properties, mark them with `[Obsolete]` and provide guidance: ```csharp [Obsolete("The metro code is no longer being maintained and should not be used.")] -public int? MetroCode { get; internal set; } +public int? MetroCode { get; init; } ``` For backward compatibility during minor version updates, add deprecated @@ -130,10 +124,13 @@ Versions"). #### 5. **MaxMind DB Attributes** -Classes that are deserialized from MMDB databases use special attributes: +Records that are deserialized from MMDB databases use special attributes on +properties: -- `[Constructor]`: Marks the constructor used for database deserialization -- `[Parameter("field_name")]`: Maps constructor parameter to database field +- `[MapKey("field_name")]`: Maps a property to a database field (replaces + `[Parameter]`) +- `[MapKey("field_name", true)]`: Maps a nested object, passing constructor arg + `true` to indicate sub-object construction - `[Inject("field_name")]`: Injects metadata like IP address - `[Network]`: Injects the network information for the IP @@ -189,66 +186,28 @@ When adding new fields to responses: ```csharp [JsonInclude] [JsonPropertyName("field_name")] - [Parameter("field_name")] // If supported by database - public TypeName? FieldName { get; internal set; } + [MapKey("field_name")] // If supported by database + public TypeName? FieldName { get; init; } ``` -2. **Update constructor(s)** to include the new parameter with a default value: +2. **No constructor changes needed** — records use property initialization, so + adding a new `init` property with a default value is not a breaking change. - ```csharp - public ModelClass( - // ... existing parameters ... - TypeName? fieldName = null // New parameter - ) - { - // ... existing assignments ... - FieldName = fieldName; - } - ``` +3. **Update tests** with assertions for the new field -3. **For minor version releases**: Add a deprecated constructor matching the old - signature to avoid breaking changes (see next section) - -4. **Update tests** with assertions for the new field - -5. **Update `releasenotes.md`** with the change +4. **Update `releasenotes.md`** with the change ### Avoiding Breaking Changes in Minor Versions -When adding a new field to an existing model class during a **minor version -release** (e.g., 5.x.0 → 5.y.0), maintain backward compatibility for users -constructing these models directly. +With records using `init` properties and object initializer syntax, adding new +properties is inherently non-breaking — consumers using +`new Traits { Domain = "example.com" }` are unaffected by new properties. -**The Solution:** Add a deprecated constructor that matches the old signature: - -```csharp -public class Traits -{ - // New constructor with added parameter - public Traits( - string? domain = null, - double? ipRiskSnapshot = null, // NEW FIELD - string? organization = null - ) - { - Domain = domain; - IpRiskSnapshot = ipRiskSnapshot; - Organization = organization; - } - - // Deprecated constructor for backward compatibility - [Obsolete("Use constructor with ipRiskSnapshot parameter")] - public Traits( - string? domain = null, - string? organization = null - ) : this(domain, null, organization) // Call new constructor with null for new field - { - } -} -``` +**What IS breaking in minor versions:** -**For Major Versions:** You do NOT need to add the deprecated constructor - -breaking changes are expected in major version bumps (e.g., 5.x.0 → 6.0.0). +- Removing or renaming existing properties +- Changing the type of existing properties +- Changing default values of existing properties ### Adding New Response Types @@ -333,23 +292,31 @@ Different target frameworks may require different approaches: #endif ``` -### Pattern: MaxMind DB Constructor Date Parsing +### Pattern: MaxMind DB Date Parsing with Backing Fields -For date fields in MMDB databases, provide two constructors: +For date fields stored as strings in MMDB databases, use a backing field with an +internal string property for deserialization: ```csharp -// Constructor for database deserialization (string) -[Constructor] -public Response( - [Parameter("date_field")] string? dateField = null -) : this(dateField == null ? null : DateOnly.Parse(dateField)) -{ } - -// Primary constructor (parsed date) -public Response(DateOnly? dateField = null) +#if NET6_0_OR_GREATER +private DateOnly? _networkLastSeen; + +[JsonInclude] +[JsonPropertyName("network_last_seen")] +public DateOnly? NetworkLastSeen { - DateField = dateField; + get => _networkLastSeen; + init => _networkLastSeen = value; } + +[JsonIgnore] +[MapKey("network_last_seen")] +internal string? NetworkLastSeenString +{ + get => _networkLastSeen?.ToString("o"); + init => _networkLastSeen = value == null ? null : DateOnly.Parse(value); +} +#endif ``` ## Code Quality @@ -369,7 +336,8 @@ The project enforces strict code quality standards: - Follow C# naming conventions (PascalCase for public members, camelCase for parameters) - Use XML documentation for all public types and members -- Keep constructors organized with optional parameters and defaults +- Prefer alphabetical ordering for `init` properties unless there is a + preexisting logical grouping ## Version Requirements @@ -390,4 +358,4 @@ The project enforces strict code quality standards: --- -_Last Updated: 2025-11-06_ +_Last Updated: 2026-03-13_ From 4ab5b699040201cfb16fc27cbeae3d69506ba7aa Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 15:54:04 +0000 Subject: [PATCH 10/15] Fix XML doc issues in NamedEntity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix two pre-existing doc issues: - cred="GeoNameId" → cref="GeoNameId" (2 occurrences) - "The GeoName ID for the city." → "for the entity." since NamedEntity is the base for City, Continent, Country, Subdivision, etc. Co-Authored-By: Claude Opus 4.6 --- MaxMind.GeoIP2/Model/NamedEntity.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MaxMind.GeoIP2/Model/NamedEntity.cs b/MaxMind.GeoIP2/Model/NamedEntity.cs index 0f048990..2d3f6759 100644 --- a/MaxMind.GeoIP2/Model/NamedEntity.cs +++ b/MaxMind.GeoIP2/Model/NamedEntity.cs @@ -15,7 +15,7 @@ public abstract record NamedEntity /// from locale codes to the name in that locale. Don't use any of /// these names as a database or dictionary key. Use the /// + /// cref="GeoNameId" /> /// or relevant code instead. /// [JsonInclude] @@ -25,7 +25,7 @@ public abstract record NamedEntity = new Dictionary(); /// - /// The GeoName ID for the city. + /// The GeoName ID for the entity. /// [JsonInclude] [JsonPropertyName("geoname_id")] @@ -44,7 +44,7 @@ public abstract record NamedEntity /// or DatabaseReader. Don't use any of /// these names as a database or dictionary key. Use the /// + /// cref="GeoNameId" /> /// or relevant code instead. /// [JsonIgnore] From 0e2983eed663d15bab9ef9ea14b4dbacea10b7ae Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 15:54:18 +0000 Subject: [PATCH 11/15] Add error context for DateOnly.Parse in AnonymousPlusResponse DateOnly.Parse(value) throws a bare FormatException with no context about which field or response type failed. Use TryParse with a descriptive GeoIP2Exception instead. Co-Authored-By: Claude Opus 4.6 --- MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs b/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs index a11c40f0..86215dda 100644 --- a/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs +++ b/MaxMind.GeoIP2/Responses/AnonymousPlusResponse.cs @@ -1,4 +1,5 @@ using MaxMind.Db; +using MaxMind.GeoIP2.Exceptions; using System; using System.Text.Json.Serialization; @@ -43,7 +44,9 @@ public DateOnly? NetworkLastSeen internal string? NetworkLastSeenString { get => _networkLastSeen?.ToString("o"); - init => _networkLastSeen = value == null ? null : DateOnly.Parse(value); + init => _networkLastSeen = value == null ? null + : DateOnly.TryParse(value, out var result) ? result + : throw new GeoIP2Exception($"Could not parse 'network_last_seen' value '{value}' as a valid date."); } #endif From 067656b43c9c064a84a4372d31b860697d59bc0d Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 15:54:30 +0000 Subject: [PATCH 12/15] Fix MostSpecificSubdivision locale on empty fallback When Subdivisions is empty, MostSpecificSubdivision returns new Subdivision() which defaults to Locales = ["en"] instead of the response's actual locales. Use City.Locales for consistency. Co-Authored-By: Claude Opus 4.6 --- MaxMind.GeoIP2/Responses/AbstractCityResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs b/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs index 38d6739f..7512f75a 100644 --- a/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs +++ b/MaxMind.GeoIP2/Responses/AbstractCityResponse.cs @@ -55,7 +55,7 @@ public abstract record AbstractCityResponse : AbstractCountryResponse /// returns an empty object. /// [JsonIgnore] - public Subdivision MostSpecificSubdivision => Subdivisions.Count == 0 ? new() : Subdivisions[Subdivisions.Count - 1]; + public Subdivision MostSpecificSubdivision => Subdivisions.Count == 0 ? new() { Locales = City.Locales } : Subdivisions[Subdivisions.Count - 1]; /// internal override AbstractResponse WithLocales(IReadOnlyList locales) From d1ec6df23d99ffe0395322c733337f4c369c5f66 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 10:18:36 -0700 Subject: [PATCH 13/15] Add Memory mode changes to release notes And remove release marker for unrelreased 5.5.0. --- releasenotes.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/releasenotes.md b/releasenotes.md index 381de461..c795c5c5 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -20,10 +20,14 @@ City, Country, Continent, Subdivision, RepresentedCountry) has been removed. Records provide a compiler-generated `ToString()` that outputs all property values. Use the `Name` property directly instead. +- **BREAKING:** `FileAccessMode.Memory` and the `DatabaseReader(Stream)` + constructor now use anonymous memory-mapped files internally instead of + `byte[]`. This removes the previous ~2.1 GiB size limitation but may break + environments where memory-mapped files are not supported, such as WASM/browser + runtimes, some mobile/sandboxed runtimes, or hardened containers with + restricted shared-memory syscalls. Non-seekable streams also now require a + writable temp directory. - Added `InternalsVisibleTo` for `MaxMind.MinFraud` assembly. - -## 5.5.0 - - `AnonymousPlus` and `TryAnonymousPlus` methods have been added to `IGeoIP2DatabaseReader`. These methods were previously only available on `DatabaseReader`. From 75f081a335975549700504e2ad7fc96b0add503f Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 17:23:40 +0000 Subject: [PATCH 14/15] Update CLAUDE.md guidance for records --- CLAUDE.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6176a9fb..2aff502e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,10 +99,6 @@ When deprecating properties, mark them with `[Obsolete]` and provide guidance: public int? MetroCode { get; init; } ``` -For backward compatibility during minor version updates, add deprecated -constructors that match old signatures (see "Avoiding Breaking Changes in Minor -Versions"). - #### 4. **Database vs Web Service Architecture** **Database Reader:** @@ -129,8 +125,9 @@ properties: - `[MapKey("field_name")]`: Maps a property to a database field (replaces `[Parameter]`) -- `[MapKey("field_name", true)]`: Maps a nested object, passing constructor arg - `true` to indicate sub-object construction +- `[MapKey("field_name", true)]`: Maps a single nested sub-object. The `true` + flag tells the deserializer to construct the nested object from a sub-map + rather than a scalar field - `[Inject("field_name")]`: Injects metadata like IP address - `[Network]`: Injects the network information for the IP @@ -295,7 +292,8 @@ Different target frameworks may require different approaches: ### Pattern: MaxMind DB Date Parsing with Backing Fields For date fields stored as strings in MMDB databases, use a backing field with an -internal string property for deserialization: +internal string property for deserialization. Invalid values should throw a +`GeoIP2Exception` with enough context to identify the field and bad value: ```csharp #if NET6_0_OR_GREATER @@ -314,7 +312,10 @@ public DateOnly? NetworkLastSeen internal string? NetworkLastSeenString { get => _networkLastSeen?.ToString("o"); - init => _networkLastSeen = value == null ? null : DateOnly.Parse(value); + init => _networkLastSeen = value == null ? null + : DateOnly.TryParse(value, out var result) ? result + : throw new GeoIP2Exception( + $"Could not parse 'network_last_seen' value '{value}' as a valid date."); } #endif ``` From 101009a4ab9de1d6690203e366f3340db5ef1d75 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 13 Mar 2026 17:24:56 +0000 Subject: [PATCH 15/15] Fix XML doc cref references in model types --- MaxMind.GeoIP2/Model/City.cs | 2 +- MaxMind.GeoIP2/Model/Continent.cs | 2 +- MaxMind.GeoIP2/Model/Country.cs | 2 +- MaxMind.GeoIP2/Model/RepresentedCountry.cs | 2 +- MaxMind.GeoIP2/Model/Subdivision.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MaxMind.GeoIP2/Model/City.cs b/MaxMind.GeoIP2/Model/City.cs index c9140aae..3a85a586 100644 --- a/MaxMind.GeoIP2/Model/City.cs +++ b/MaxMind.GeoIP2/Model/City.cs @@ -8,7 +8,7 @@ namespace MaxMind.GeoIP2.Model /// /// /// Do not use any of the city names as a database or dictionary - /// key. Use the instead. + /// key. Use the instead. /// public record City : NamedEntity { diff --git a/MaxMind.GeoIP2/Model/Continent.cs b/MaxMind.GeoIP2/Model/Continent.cs index 7898094b..bba219cd 100644 --- a/MaxMind.GeoIP2/Model/Continent.cs +++ b/MaxMind.GeoIP2/Model/Continent.cs @@ -6,7 +6,7 @@ namespace MaxMind.GeoIP2.Model /// /// Contains data for the continent record associated with an IP address. /// Do not use any of the continent names as a database or dictionary - /// key. Use the or + /// key. Use the or /// instead. /// public record Continent : NamedEntity diff --git a/MaxMind.GeoIP2/Model/Country.cs b/MaxMind.GeoIP2/Model/Country.cs index ca3df43f..6fda592c 100644 --- a/MaxMind.GeoIP2/Model/Country.cs +++ b/MaxMind.GeoIP2/Model/Country.cs @@ -6,7 +6,7 @@ namespace MaxMind.GeoIP2.Model /// /// Contains data for the country record associated with an IP address. /// Do not use any of the country names as a database or dictionary - /// key. Use the or + /// key. Use the or /// instead. /// public record Country : NamedEntity diff --git a/MaxMind.GeoIP2/Model/RepresentedCountry.cs b/MaxMind.GeoIP2/Model/RepresentedCountry.cs index af2bc0d4..5bf769ea 100644 --- a/MaxMind.GeoIP2/Model/RepresentedCountry.cs +++ b/MaxMind.GeoIP2/Model/RepresentedCountry.cs @@ -9,7 +9,7 @@ namespace MaxMind.GeoIP2.Model /// the IP's represented country. The represented country is the country /// represented by something like a military base. /// Do not use any of the country names as a database or dictionary - /// key. Use the or + /// key. Use the or /// instead. /// public record RepresentedCountry : Country diff --git a/MaxMind.GeoIP2/Model/Subdivision.cs b/MaxMind.GeoIP2/Model/Subdivision.cs index c0d5bcf5..bec6e0b4 100644 --- a/MaxMind.GeoIP2/Model/Subdivision.cs +++ b/MaxMind.GeoIP2/Model/Subdivision.cs @@ -6,7 +6,7 @@ namespace MaxMind.GeoIP2.Model /// /// Contains data for the subdivisions associated with an IP address. /// Do not use any of the subdivision names as a database or dictionary - /// key. Use the or + /// key. Use the or /// instead. /// public record Subdivision : NamedEntity