Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions StrEnum.EntityFrameworkCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<PackageReference Include="StrEnum" Version="[2.0.0,3.0.0)" />
<PackageReference Include="StrEnum" Version="[2.1.0,3.0.0)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace StrEnum.EntityFrameworkCore.IntegrationTests;

/// <summary>
/// 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 — <see cref="StringEnum{TEnum}"/> implements
/// <see cref="IComparable{T}"/> and <see cref="IComparable"/> as of StrEnum 2.1.0.
/// </summary>
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<NationalRecord> NationalRecords => Set<NationalRecord>();

public RecordsContext(SqliteConnection connection) => _connection = connection;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlite(_connection)
.UseStringEnums();
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<NationalRecord>()
.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<Country>();

// 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<StringEnumValueConverter<Country>>();
}

[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");
}
}
18 changes: 18 additions & 0 deletions test/StrEnum.EntityFrameworkCore.IntegrationTests/Country.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace StrEnum.EntityFrameworkCore.IntegrationTests;

public class Country : StringEnum<Country>
{
public static readonly Country Ukraine = Define("UKR");
public static readonly Country SouthAfrica = Define("ZAF");
public static readonly Country Norway = Define("NOR");
}

/// <summary>
/// A national record, keyed on the composite of the country and the year it was set.
/// </summary>
public class NationalRecord
{
public Country Country { get; set; } = null!;
public int Year { get; set; }
public string Athlete { get; set; } = "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="StrEnum" Version="2.1.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\StrEnum.EntityFrameworkCore\StrEnum.EntityFrameworkCore.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="StrEnum" Version="2.0.3" />
<PackageReference Include="StrEnum" Version="2.1.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading