diff --git a/Dockerfile b/Dockerfile
index b5ba1d6..5074fb0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /source
COPY *.sln .
COPY src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj ./src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj
COPY test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj ./test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj
+COPY test/StrEnum.EntityFrameworkCore.IntegrationTests/StrEnum.EntityFrameworkCore.IntegrationTests.csproj ./test/StrEnum.EntityFrameworkCore.IntegrationTests/StrEnum.EntityFrameworkCore.IntegrationTests.csproj
RUN dotnet restore
# copy everything else and build app
diff --git a/StrEnum.EntityFrameworkCore.sln b/StrEnum.EntityFrameworkCore.sln
index c968c43..386f523 100644
--- a/StrEnum.EntityFrameworkCore.sln
+++ b/StrEnum.EntityFrameworkCore.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StrEnum.EntityFrameworkCore
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StrEnum.EntityFrameworkCore.UnitTests", "test\StrEnum.EntityFrameworkCore.UnitTests\StrEnum.EntityFrameworkCore.UnitTests.csproj", "{25F8E9AF-2901-4AC7-B31E-D132C6130F93}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StrEnum.EntityFrameworkCore.IntegrationTests", "test\StrEnum.EntityFrameworkCore.IntegrationTests\StrEnum.EntityFrameworkCore.IntegrationTests.csproj", "{6C1E4B57-0D3A-42F8-9A64-1B8E5F2C7D40}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{877F93E7-A8AB-4362-8FEE-A9E90E1A96D0}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
@@ -26,6 +28,10 @@ Global
{25F8E9AF-2901-4AC7-B31E-D132C6130F93}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25F8E9AF-2901-4AC7-B31E-D132C6130F93}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25F8E9AF-2901-4AC7-B31E-D132C6130F93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C1E4B57-0D3A-42F8-9A64-1B8E5F2C7D40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C1E4B57-0D3A-42F8-9A64-1B8E5F2C7D40}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C1E4B57-0D3A-42F8-9A64-1B8E5F2C7D40}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C1E4B57-0D3A-42F8-9A64-1B8E5F2C7D40}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj b/src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj
index 8b87a76..8c2dc6b 100644
--- a/src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj
+++ b/src/StrEnum.EntityFrameworkCore/StrEnum.EntityFrameworkCore.csproj
@@ -69,6 +69,6 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
\ No newline at end of file
diff --git a/test/StrEnum.EntityFrameworkCore.IntegrationTests/CompositePrimaryKeyTests.cs b/test/StrEnum.EntityFrameworkCore.IntegrationTests/CompositePrimaryKeyTests.cs
new file mode 100644
index 0000000..abee0df
--- /dev/null
+++ b/test/StrEnum.EntityFrameworkCore.IntegrationTests/CompositePrimaryKeyTests.cs
@@ -0,0 +1,124 @@
+using FluentAssertions;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace StrEnum.EntityFrameworkCore.IntegrationTests;
+
+///
+/// Verifies that a string enum can take part in a composite primary key end to end: schema creation,
+/// insert, lookup by the composite key, update, and query. EF Core orders key values internally, so it
+/// rejects key types that aren't comparable — implements
+/// and as of StrEnum 2.1.0.
+///
+public class CompositePrimaryKeyTests : IDisposable
+{
+ private readonly SqliteConnection _connection;
+
+ public CompositePrimaryKeyTests()
+ {
+ _connection = new SqliteConnection("Data Source=:memory:");
+ _connection.Open();
+ }
+
+ public void Dispose() => _connection.Dispose();
+
+ private class RecordsContext : DbContext
+ {
+ private readonly SqliteConnection _connection;
+
+ public DbSet NationalRecords => Set();
+
+ public RecordsContext(SqliteConnection connection) => _connection = connection;
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder
+ .UseSqlite(_connection)
+ .UseStringEnums();
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasKey(r => new { r.Country, r.Year });
+ }
+ }
+
+ private RecordsContext CreateContext() => new(_connection);
+
+ [Fact]
+ public void Builds_a_composite_primary_key_that_includes_a_string_enum()
+ {
+ using var context = CreateContext();
+
+ var primaryKey = context.Model.FindEntityType(typeof(NationalRecord))!.FindPrimaryKey()!;
+
+ primaryKey.Properties.Select(p => p.Name).Should().Equal(nameof(NationalRecord.Country), nameof(NationalRecord.Year));
+
+ var countryProperty = primaryKey.Properties.First();
+
+ countryProperty.ClrType.Should().Be();
+
+ // UseStringEnums supplies the converter through the value converter selector rather than an
+ // explicit HasConversion call, so it surfaces on the type mapping.
+ countryProperty.GetTypeMapping().Converter.Should().BeOfType>();
+ }
+
+ [Fact]
+ public async Task Round_trips_a_row_keyed_on_a_string_enum()
+ {
+ await using (var seedContext = CreateContext())
+ {
+ await seedContext.Database.EnsureCreatedAsync();
+
+ seedContext.NationalRecords.AddRange(
+ new NationalRecord { Country = Country.Ukraine, Year = 2024, Athlete = "Olha Bilyk" },
+ new NationalRecord { Country = Country.SouthAfrica, Year = 2024, Athlete = "Thabo Nkosi" },
+ new NationalRecord { Country = Country.Norway, Year = 2025, Athlete = "Ingrid Dahl" });
+
+ await seedContext.SaveChangesAsync();
+ }
+
+ // Look the row up by the composite key, which is the path that orders key values internally.
+ await using (var updateContext = CreateContext())
+ {
+ var record = await updateContext.NationalRecords.FindAsync(Country.SouthAfrica, 2024);
+
+ record!.Athlete.Should().Be("Thabo Nkosi");
+
+ record.Athlete = "Thabo Nkosi-Mokoena";
+
+ await updateContext.SaveChangesAsync();
+ }
+
+ await using var queryContext = CreateContext();
+
+ var records = await queryContext.NationalRecords
+ .Where(r => r.Year == 2024)
+ .ToArrayAsync();
+
+ records.Should().HaveCount(2);
+ records.Single(r => r.Country == Country.SouthAfrica).Athlete.Should().Be("Thabo Nkosi-Mokoena");
+ records.Single(r => r.Country == Country.Ukraine).Athlete.Should().Be("Olha Bilyk");
+ }
+
+ [Fact]
+ public async Task Stores_the_key_column_as_the_member_value()
+ {
+ await using var context = CreateContext();
+
+ await context.Database.EnsureCreatedAsync();
+
+ context.NationalRecords.Add(new NationalRecord { Country = Country.Ukraine, Year = 2026, Athlete = "Olha Bilyk" });
+
+ await context.SaveChangesAsync();
+
+ await using var command = _connection.CreateCommand();
+ command.CommandText = "SELECT \"Country\" FROM \"NationalRecords\" WHERE \"Year\" = 2026";
+
+ var storedValue = await command.ExecuteScalarAsync();
+
+ storedValue.Should().Be("UKR");
+ }
+}
diff --git a/test/StrEnum.EntityFrameworkCore.IntegrationTests/Country.cs b/test/StrEnum.EntityFrameworkCore.IntegrationTests/Country.cs
new file mode 100644
index 0000000..78c1b2a
--- /dev/null
+++ b/test/StrEnum.EntityFrameworkCore.IntegrationTests/Country.cs
@@ -0,0 +1,18 @@
+namespace StrEnum.EntityFrameworkCore.IntegrationTests;
+
+public class Country : StringEnum
+{
+ public static readonly Country Ukraine = Define("UKR");
+ public static readonly Country SouthAfrica = Define("ZAF");
+ public static readonly Country Norway = Define("NOR");
+}
+
+///
+/// A national record, keyed on the composite of the country and the year it was set.
+///
+public class NationalRecord
+{
+ public Country Country { get; set; } = null!;
+ public int Year { get; set; }
+ public string Athlete { get; set; } = "";
+}
diff --git a/test/StrEnum.EntityFrameworkCore.IntegrationTests/StrEnum.EntityFrameworkCore.IntegrationTests.csproj b/test/StrEnum.EntityFrameworkCore.IntegrationTests/StrEnum.EntityFrameworkCore.IntegrationTests.csproj
new file mode 100644
index 0000000..35ed128
--- /dev/null
+++ b/test/StrEnum.EntityFrameworkCore.IntegrationTests/StrEnum.EntityFrameworkCore.IntegrationTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+ 10.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj b/test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj
index 4a616fa..fda9679 100644
--- a/test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj
+++ b/test/StrEnum.EntityFrameworkCore.UnitTests/StrEnum.EntityFrameworkCore.UnitTests.csproj
@@ -10,7 +10,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive