Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7808f5d
Add source-generated activation registries
oschwald Aug 1, 2026
d3d5b78
Add deterministic MMDB source generator
oschwald Aug 1, 2026
7d4ad23
Package source generation and AOT diagnostics
oschwald Aug 1, 2026
91d9732
Add packaged NativeAOT publish coverage
oschwald Aug 1, 2026
762b80e
Document NativeAOT support
oschwald Aug 1, 2026
13a7fe7
Add 5.2.0 release notes
oschwald Aug 1, 2026
18d639a
Report annotated struct models as MMDBSG012
oschwald Aug 3, 2026
fecfafb
Keep value-type AlwaysCreate members at their default
oschwald Aug 5, 2026
8d63129
Stop MMDBSG005 firing on injectable and network members
oschwald Aug 5, 2026
ff01657
Report MapKey combined with Inject or Network as MMDBSG013
oschwald Aug 5, 2026
8ba21c8
Report hidden annotated properties as MMDBSG014
oschwald Aug 5, 2026
1e7b396
Report unresolvable lookup result types as MMDBSG015
oschwald Aug 5, 2026
d170c84
Give inherited-member diagnostics a source location
oschwald Aug 5, 2026
4a0b18d
Detect required fields as well as required properties
oschwald Aug 5, 2026
222891b
Report annotated models with no deserialization constructor as MMDBSG016
oschwald Aug 5, 2026
53665eb
Enable MaxMindDbAotDiagnostics by default
oschwald Aug 5, 2026
06cd1ab
Cover the reflection fallback with the full TypeHolder shapes
oschwald Aug 5, 2026
0980e57
Make registration generic and give GeneratedMember named factories
oschwald Aug 5, 2026
71ff27b
Give CollectionRoot value equality
oschwald Aug 5, 2026
286e475
Fail the pack when the analyzer path goes stale
oschwald Aug 5, 2026
3be70e7
Add the NativeAOT projects to the solution
oschwald Aug 5, 2026
411acfd
Remove drift and leaks from the NativeAOT test script
oschwald Aug 5, 2026
deecf8d
Correct the NativeAOT documentation and release notes
oschwald Aug 5, 2026
51d8c08
Order helpers caller-before-callee
oschwald Aug 5, 2026
945747b
Make the collection precedence test prove precedence
oschwald Aug 5, 2026
d081c05
Document why the non-generic dictionary latch is safe
oschwald Aug 5, 2026
aaba6ed
Give the CI benchmark addresses that hit records
oschwald Aug 5, 2026
aa5bb57
Check out submodules for the CodeQL build
oschwald Aug 5, 2026
96c807a
Narrow MMDBSG015 to constructed generic lookup types
oschwald Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
# 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
# The NativeAOT test application copies test databases out of this
# submodule, so autobuild cannot compile the solution without it.
submodules: true
persist-credentials: false

# If this run was triggered by a pull request event, then checkout
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,43 @@ jobs:
MAXMIND_BENCHMARK_DB:
${{ github.workspace
}}/MaxMind.Db.Test/TestData/MaxMind-DB/test-data/GeoIP2-City-Test.mmdb
# Without this the benchmark falls back to 1,000 pseudo-random addresses,
# which all miss in this 26-record database: no record is found, no model
# is ever constructed, and it reports zero allocations. These addresses
# hit, so the decode path is actually measured.
MAXMIND_BENCHMARK_IP_ADDRESSES: 2.125.160.216,67.43.156.1,81.2.69.142,81.2.69.144,81.2.69.160,89.160.20.112,89.160.20.128,149.101.100.1,175.16.199.1,202.196.224.1,216.160.83.56,216.160.83.61

- name: Run tests
run: dotnet test
env:
MAXMIND_TEST_BASE_DIR: ${{ github.workspace }}/MaxMind.Db.Test

native-aot:
strategy:
matrix:
include:
- platform: ubuntu-latest
rid: linux-x64
- platform: windows-latest
rid: win-x64
- platform: macos-latest
rid: osx-arm64
runs-on: ${{ matrix.platform }}
name: NativeAOT on ${{ matrix.platform }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x

- name: Publish and run NativeAOT integration tests
shell: bash
run: bash ./dev-bin/test-native-aot.sh ${{ matrix.rid }}
3 changes: 3 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"ignores": ["MaxMind.Db.SourceGenerator/AnalyzerReleases.*.md"]
}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MaxMind.Db.SourceGenerator/AnalyzerReleases.*.md
3 changes: 3 additions & 0 deletions MaxMind.Db.Benchmark/MaxMind.Db.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@

<ItemGroup>
<ProjectReference Include="..\MaxMind.Db\MaxMind.Db.csproj" />
<ProjectReference Include="..\MaxMind.Db.SourceGenerator\MaxMind.Db.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand Down
28 changes: 28 additions & 0 deletions MaxMind.Db.NativeAot/App/MaxMind.Db.NativeAot.App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<ILLinkTreatWarningsAsErrors>true</ILLinkTreatWarningsAsErrors>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Models\MaxMind.Db.NativeAot.Models.csproj" />
<ProjectReference Include="..\..\MaxMind.Db.NetStandard.TestModels\MaxMind.Db.NetStandard.TestModels.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\MaxMind.Db.Test\TestData\MaxMind-DB\test-data\GeoIP2-City-Test.mmdb"
Link="GeoIP2-City-Test.mmdb"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest" />
<None Include="..\..\MaxMind.Db.Test\TestData\MaxMind-DB\test-data\MaxMind-DB-test-decoder.mmdb"
Link="MaxMind-DB-test-decoder.mmdb"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

</Project>
175 changes: 175 additions & 0 deletions MaxMind.Db.NativeAot/App/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.CompilerServices;
using MaxMind.Db;
using MaxMind.Db.NativeAot.Models;
using MaxMind.Db.NetStandard.TestModels;

namespace MaxMind.Db.NativeAot.App
{
internal static class Program
{
private static readonly IPAddress _decoderAddress = IPAddress.Parse("1.1.1.1");
private static readonly IPAddress _cityAddress = IPAddress.Parse("81.2.69.160");

private static void Main()
{
Check(!RuntimeFeature.IsDynamicCodeSupported, "The integration test must run as NativeAOT.");
Check(
typeof(CityResponse).Assembly != typeof(Reader).Assembly,
"Models must be generated in a separate assembly.");

foreach (var mode in new[] { FileAccessMode.MemoryMapped, FileAccessMode.Memory })
{
TestDecoderDatabase(mode);
TestCityDatabase(mode);
TestReflectionFallbackFailsWithGuidance(mode);
}

Console.WriteLine("NativeAOT integration tests passed.");
}

private static void TestDecoderDatabase(FileAccessMode mode)
{
var database = Path.Combine(AppContext.BaseDirectory, "MaxMind-DB-test-decoder.mmdb");
using var reader = new Reader(database, mode);

Check(reader.Metadata.DatabaseType == "MaxMind DB Decoder Test", "Unexpected metadata.");

var dictionary = reader.Find<Dictionary<string, object>>(_decoderAddress) ??
throw new InvalidOperationException("Dictionary lookup returned null.");
Check(
(string)dictionary["utf8_string"] == "unicode! ☯ - ♫",
"Dictionary string did not decode.");

var concurrentDictionary =
reader.Find<ConcurrentDictionary<string, object>>(_decoderAddress) ??
throw new InvalidOperationException(
"ConcurrentDictionary lookup returned null.");
Check(
(string)concurrentDictionary["utf8_string"] == "unicode! ☯ - ♫",
"Source-generated top-level dictionary did not decode.");

var constructorModel = reader.Find<DecoderConstructorModel>(_decoderAddress) ??
throw new InvalidOperationException("Constructor model lookup returned null.");
Check(
constructorModel.Utf8String == "unicode! ☯ - ♫",
"Constructor model string did not decode.");
Check(
SequenceEquals(constructorModel.Array, 1, 2, 3),
"IReadOnlyList did not decode.");
Check(
NestedStringEquals(constructorModel.Map, "hello"),
"IReadOnlyDictionary did not decode.");

var netStandardModel = reader.Find<NetStandardModel>(_decoderAddress) ??
throw new InvalidOperationException(".NET Standard model lookup returned null.");
Check(
netStandardModel.Utf8String == "unicode! ☯ - ♫",
".NET Standard model did not decode.");

var propertyModel = reader.Find<DecoderPropertyModel>(_decoderAddress) ??
throw new InvalidOperationException("Property model lookup returned null.");
Check(SequenceEquals(propertyModel.Array, 1, 2, 3), "ICollection did not decode.");
Check(
NestedStringEquals(propertyModel.Map, "hello"),
"Dictionary did not decode.");

var concreteModel = reader.Find<DecoderConcreteCollectionModel>(_decoderAddress) ??
throw new InvalidOperationException("Concrete collection model lookup returned null.");
Check(SequenceEquals(concreteModel.Array, 1, 2, 3), "LinkedList did not decode.");
Check(
NestedStringEquals(concreteModel.Map, "hello"),
"Concrete dictionary did not decode.");

var count = 0;
foreach (var _ in reader.FindAll<DecoderConstructorModel>())
{
count++;
}
Check(count == 26, $"FindAll returned {count} records instead of 26.");
}

private static void TestCityDatabase(FileAccessMode mode)
{
var database = Path.Combine(AppContext.BaseDirectory, "GeoIP2-City-Test.mmdb");
var injectables = new InjectableValues();
injectables.AddValue("locales", (IReadOnlyList<string>)["en"]);
using var reader = new Reader(database, mode);

var response = reader.Find<CityResponse>(_cityAddress, injectables) ??
throw new InvalidOperationException("City lookup returned null.");
Check(response.City.Name == "London", "City name did not decode.");
Check(response.Subdivisions.Count == 1, "Subdivisions did not decode.");
Check(
response.Subdivisions[0].Name == "England",
"Subdivision name did not decode.");
Check(
response.Traits.Network?.ToString() == "81.2.69.160/27",
"Network did not decode.");
}

// ReflectionFallbackModel has no generated registration, so this is the only
// coverage of what the reflection fallback does once trimmed and published as
// NativeAOT; no analyzer reports that path. Full trimming removes the members
// reflection needs, so the lookup fails before Expression.Compile is ever
// reached. What matters is that the failure stays actionable.
private static void TestReflectionFallbackFailsWithGuidance(FileAccessMode mode)
{
var database = Path.Combine(AppContext.BaseDirectory, "MaxMind-DB-test-decoder.mmdb");
using var reader = new Reader(database, mode);

try
{
reader.Find<ReflectionFallbackModel>(_decoderAddress);
}
catch (DeserializationException ex)
{
Check(
ex.Message.Contains(nameof(ReflectionFallbackModel)),
"The fallback failure must name the model.");
Check(
ex.Message.Contains("rebuild the assembly that declares the model"),
"The fallback failure must point at the source generator.");
return;
}

throw new InvalidOperationException(
"Decoding a model without a generated registration must fail once trimmed.");
}

private static bool SequenceEquals(IEnumerable<long> values, params long[] expected)
{
var index = 0;
foreach (var value in values)
{
if (index >= expected.Length || value != expected[index])
{
return false;
}
index++;
}
return index == expected.Length;
}

private static bool NestedStringEquals(
IReadOnlyDictionary<string, object> map,
string expected
)
=> map.TryGetValue("mapX", out var nested) &&
nested is IReadOnlyDictionary<string, object> inner &&
inner.TryGetValue("utf8_stringX", out var value) &&
(value as string) == expected;

private static void Check(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
}
}
32 changes: 32 additions & 0 deletions MaxMind.Db.NativeAot/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project>

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup Condition="'$(MaxMindDbPackageVersion)' != ''">
<PackageReference Include="MaxMind.Db" Version="$(MaxMindDbPackageVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(MaxMindDbPackageVersion)' == ''">
<ProjectReference Include="$(MSBuildThisFileDirectory)..\MaxMind.Db\MaxMind.Db.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\MaxMind.Db.SourceGenerator\MaxMind.Db.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
</ItemGroup>

<Target Name="VerifyMaxMindDbAotDiagnostics"
BeforeTargets="CoreCompile"
Condition="'$(MaxMindDbPackageVersion)' != '' and '$(MaxMindDbAotDiagnostics)' != 'true'">
<Error Text="The packaged MaxMind.Db analyzer did not enable NativeAOT diagnostics for $(MSBuildProjectName)." />
</Target>

</Project>
19 changes: 19 additions & 0 deletions MaxMind.Db.NativeAot/Models/MaxMind.Db.NativeAot.Models.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- Written out so VerifyMaxMindDbGeneratedRegistrations can confirm the
generator ran. The App consumes a packed MaxMind.Db, so this is what
catches the analyzer going missing from the package. -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<Target Name="VerifyMaxMindDbGeneratedRegistrations" AfterTargets="CoreCompile">
<ItemGroup>
<MaxMindDbGeneratedRegistration
Include="$(IntermediateOutputPath)generated/**/MaxMind.Db.SourceGenerator.g.cs" />
</ItemGroup>
<Error Condition="'@(MaxMindDbGeneratedRegistration)' == ''"
Text="The MaxMind.Db source generator produced no registrations for $(MSBuildProjectName). Every model in this assembly would fall back to reflection, which is unsupported after trimming or with NativeAOT." />
</Target>

</Project>
Loading
Loading