diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml index 3f1de2d..9f262ca 100644 --- a/.github/workflows/build-samples.yml +++ b/.github/workflows/build-samples.yml @@ -30,6 +30,7 @@ on: - "ef-core/advanced-modeling-performance/**" - "ef-core/modern-data-access-dotnet/**" - "ef-core/multitenancy-softdelete-auditing/**" + - "ef-core/testing-sqlite-inmemory-vs-inmemory/**" - ".github/workflows/build-samples.yml" pull_request: @@ -60,6 +61,7 @@ on: - "ef-core/advanced-modeling-performance/**" - "ef-core/modern-data-access-dotnet/**" - "ef-core/multitenancy-softdelete-auditing/**" + - "ef-core/testing-sqlite-inmemory-vs-inmemory/**" - ".github/workflows/build-samples.yml" workflow_dispatch: @@ -1870,3 +1872,143 @@ jobs: exit 1 fi + test-ef-core-provider-contrast: + name: Test EF Core provider contrast sample + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Install .NET 10 SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + + - name: Restore + run: > + dotnet restore + ef-core/testing-sqlite-inmemory-vs-inmemory/EfCoreProviderContrastMinimal.slnx + + - name: Build + run: > + dotnet build + ef-core/testing-sqlite-inmemory-vs-inmemory/EfCoreProviderContrastMinimal.slnx + --configuration Release + --no-restore + + - name: Test + run: > + dotnet test + ef-core/testing-sqlite-inmemory-vs-inmemory/EfCoreProviderContrastMinimal.slnx + --configuration Release + --no-build + + - name: Verify EF Core provider baseline + shell: bash + run: | + project="ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/EfCoreProviderContrastMinimal.csproj" + + test "$( + grep \ + --count \ + ' + + + diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/README.md b/ef-core/testing-sqlite-inmemory-vs-inmemory/README.md new file mode 100644 index 0000000..f0c1270 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/README.md @@ -0,0 +1,199 @@ +# EF Core SQLite vs InMemory Relational-Behavior Contrast + +A focused .NET 10 / EF Core 10 test-provider lab showing four concrete +relational behaviors that differ between SQLite in-memory and the EF Core +InMemory provider. + +## Full tutorial + +[EF Core Testing: SQLite In-Memory vs InMemory Provider — Which One Can You Actually Trust?](https://www.dotnet-guide.com/tutorials/ef-core/testing-sqlite-inmemory-vs-inmemory/) + +## Version baseline + +```text +.NET 10 +Microsoft.EntityFrameworkCore.Sqlite 10.0.11 +Microsoft.EntityFrameworkCore.InMemory 10.0.11 +``` + +Provider behavior is version-sensitive. The assertions in this sample are +reviewed against this pinned baseline. + +## Why InMemory appears in this repository + +Microsoft discourages using the EF Core InMemory provider as a general database +fake for relational applications. + +It is included here because the tutorial specifically compares its behavior +with SQLite. + +This is a contrast sample, not a recommendation to introduce InMemory into new +integration-test suites. + +## What the sample compares + +```text + InMemory SQLite :memory: +Unique index accepts rejects +Foreign key accepts rejects +Transaction unsupported rollback works +DB cascade orphan remains child removed +``` + +## One shared model + +Both providers use exactly the same: + +```text +Project +TaskItem +unique (Name, OwnerId) index +Project -> Tasks foreign key +DeleteBehavior.Cascade +``` + +Only the provider changes. + +## SQLite connection lifetime + +The SQLite helper opens one low-level in-memory connection and leaves it open +until the scenario finishes. + +Closing the connection destroys the in-memory database. + +## Foreign keys + +The sample uses: + +```text +Data Source=:memory:;Foreign Keys=True +``` + +to make FK enforcement explicit. + +The default EF Core SQLite native bundle currently enables foreign keys by +default, so a manual PRAGMA is not universally required with the standard +bundle. + +## Database-side cascade + +The cascade scenario intentionally deletes a Project from a fresh context that +does not load the child TaskItem. + +That distinction matters. + +EF Core can apply client-side cascade behavior to dependents already tracked in +a DbContext. The test here specifically asks whether the backing database/store +enforces the FK cascade when the dependent isn't tracked. + +## Transactions + +The EF InMemory provider doesn't support transactions. + +By default, attempting to start one throws. + +EF can be configured to ignore the transaction warning, but doing so still +does not provide rollback semantics. + +SQLite executes a real transaction in this sample. + +## Concurrency is deliberately not part of the contrast + +Current EF Core InMemory code checks configured concurrency tokens and can +throw `DbUpdateConcurrencyException`. + +The full website tutorial must not describe current InMemory as simply ignoring +concurrency tokens. + +SQLite still differs from SQL Server for database-generated rowversion-style +tokens. + +## Query translation is also not reduced to a one-line matrix + +InMemory and relational providers use different query pipelines and may fail or +behave differently. + +Modern EF Core doesn't generally turn arbitrary non-translatable expressions +into silent client evaluation. + +Provider-specific query behavior should be tested against the provider that +matters. + +## SQLite is still a fake for another production engine + +SQLite relational behavior is more realistic than EF InMemory for these four +cases, but SQLite is not SQL Server or PostgreSQL. + +Differences include: + +- case sensitivity/collation; +- SQL dialect; +- provider-specific methods; +- native data types; +- migrations; +- concurrency-token generation; +- JSON/provider extensions. + +Use the real production database in automated tests when those differences +matter. + +## Deterministic output + +```text +EF Core Test Provider Contrast Lab +Unique index: InMemory=accepted | SQLite=rejected +Foreign key: InMemory=accepted | SQLite=rejected +Transaction: InMemory=unsupported | SQLite=rollback-ok +Database cascade: InMemory=orphan-remains | SQLite=child-removed +Guidance: relational behavior -> production provider when practical; SQLite remains a test double. +``` + +## Restore, build, test + +```powershell +dotnet restore ` + .\EfCoreProviderContrastMinimal.slnx + +dotnet build ` + .\EfCoreProviderContrastMinimal.slnx ` + --configuration Release ` + --no-restore + +dotnet test ` + .\EfCoreProviderContrastMinimal.slnx ` + --configuration Release ` + --no-build +``` + +## Run + +```powershell +dotnet run ` + --project .\src\EfCoreProviderContrastMinimal\EfCoreProviderContrastMinimal.csproj ` + --configuration Release ` + --no-build +``` + +## Deliberately omitted + +- web API; +- concurrency-provider matrix; +- SQL Server; +- PostgreSQL; +- Testcontainers; +- migrations; +- raw SQL; +- JSON; +- temporal tables; +- performance benchmarking. + +## Verification + +- Target framework: .NET 10 +- EF Core baseline: 10.0.11 +- Direct app packages: 2 +- Tests: 8 +- External services: none +- Physical database files: none +- Performance claims: none +- Last reviewed: 2026-08-17 \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Data/ProviderDbContext.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Data/ProviderDbContext.cs new file mode 100644 index 0000000..babae35 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Data/ProviderDbContext.cs @@ -0,0 +1,93 @@ +using EfCoreProviderContrastMinimal.Models; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreProviderContrastMinimal.Data; + +public sealed class ProviderDbContext( + DbContextOptions< + ProviderDbContext> + options) : + DbContext( + options) +{ + public DbSet Projects => + Set(); + + public DbSet Tasks => + Set(); + + protected override void + OnModelCreating( + ModelBuilder modelBuilder) + { + modelBuilder.Entity( + project => + { + project.HasKey( + entity => + entity.Id); + + project.Property( + entity => + entity.Id) + .ValueGeneratedNever(); + + project.Property( + entity => + entity.Name) + .HasMaxLength( + 80) + .IsRequired(); + + project.Property( + entity => + entity.OwnerId) + .HasMaxLength( + 80) + .IsRequired(); + + project.HasIndex( + entity => + new + { + entity.Name, + entity.OwnerId + }) + .IsUnique() + .HasDatabaseName( + "UX_Projects_Name_Owner"); + + project.HasMany( + entity => + entity.Tasks) + .WithOne( + task => + task.Project) + .HasForeignKey( + task => + task.ProjectId) + .OnDelete( + DeleteBehavior.Cascade); + }); + + modelBuilder.Entity( + task => + { + task.HasKey( + entity => + entity.Id); + + task.Property( + entity => + entity.Id) + .ValueGeneratedNever(); + + task.Property( + entity => + entity.Title) + .HasMaxLength( + 120) + .IsRequired(); + }); + } +} \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/EfCoreProviderContrastMinimal.csproj b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/EfCoreProviderContrastMinimal.csproj new file mode 100644 index 0000000..c08e9ab --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/EfCoreProviderContrastMinimal.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + true + + + + + + + + + \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Models/ProviderModels.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Models/ProviderModels.cs new file mode 100644 index 0000000..d4d9c1e --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Models/ProviderModels.cs @@ -0,0 +1,54 @@ +namespace EfCoreProviderContrastMinimal.Models; + +public sealed class Project +{ + public int Id + { + get; + set; + } + + public string Name + { + get; + set; + } = ""; + + public string OwnerId + { + get; + set; + } = ""; + + public List Tasks + { + get; + } = []; +} + +public sealed class TaskItem +{ + public int Id + { + get; + set; + } + + public string Title + { + get; + set; + } = ""; + + public int ProjectId + { + get; + set; + } + + public Project Project + { + get; + set; + } = null!; +} \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Program.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Program.cs new file mode 100644 index 0000000..faeb859 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Program.cs @@ -0,0 +1,35 @@ +using EfCoreProviderContrastMinimal.Services; + +ProviderContrastResult result = + await ProviderContrast + .RunAsync(); + +Console.WriteLine( + "EF Core Test Provider Contrast Lab"); + +Console.WriteLine( + $"Unique index: InMemory=" + + $"{(result.UniqueConstraint.InMemoryAccepted ? "accepted" : "rejected")} | " + + $"SQLite=" + + $"{(result.UniqueConstraint.SqliteRejected ? "rejected" : "accepted")}"); + +Console.WriteLine( + $"Foreign key: InMemory=" + + $"{(result.ForeignKey.InMemoryAccepted ? "accepted" : "rejected")} | " + + $"SQLite=" + + $"{(result.ForeignKey.SqliteRejected ? "rejected" : "accepted")}"); + +Console.WriteLine( + $"Transaction: InMemory=" + + $"{(result.Transaction.InMemoryUnsupported ? "unsupported" : "supported")} | " + + $"SQLite=" + + $"{(result.Transaction.SqliteRolledBack ? "rollback-ok" : "rollback-failed")}"); + +Console.WriteLine( + $"Database cascade: InMemory=" + + $"{(result.Cascade.InMemoryOrphanRemains ? "orphan-remains" : "child-removed")} | " + + $"SQLite=" + + $"{(result.Cascade.SqliteChildRemoved ? "child-removed" : "orphan-remains")}"); + +Console.WriteLine( + "Guidance: relational behavior -> production provider when practical; SQLite remains a test double."); diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/InMemoryTestDatabase.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/InMemoryTestDatabase.cs new file mode 100644 index 0000000..17af8a3 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/InMemoryTestDatabase.cs @@ -0,0 +1,60 @@ +using EfCoreProviderContrastMinimal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace EfCoreProviderContrastMinimal.Providers; + +public sealed class + InMemoryTestDatabase : + IAsyncDisposable +{ + private readonly + InMemoryDatabaseRoot + _root = + new(); + + private readonly + string + _databaseName = + "EfCoreProviderContrast"; + + private readonly + DbContextOptions< + ProviderDbContext> + _options; + + public InMemoryTestDatabase() + { + _options = + new DbContextOptionsBuilder< + ProviderDbContext>() + .UseInMemoryDatabase( + _databaseName, + _root) + .EnableDetailedErrors() + .Options; + + using + ProviderDbContext context = + CreateContext(); + + context.Database + .EnsureCreated(); + } + + public ProviderDbContext + CreateContext() => + new( + _options); + + public async ValueTask + DisposeAsync() + { + await using + ProviderDbContext context = + CreateContext(); + + await context.Database + .EnsureDeletedAsync(); + } +} \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/SqliteTestDatabase.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/SqliteTestDatabase.cs new file mode 100644 index 0000000..dc04f2d --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Providers/SqliteTestDatabase.cs @@ -0,0 +1,94 @@ +using EfCoreProviderContrastMinimal.Data; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreProviderContrastMinimal.Providers; + +public sealed class + SqliteTestDatabase : + IAsyncDisposable +{ + private readonly + SqliteConnection + _connection; + + private readonly + DbContextOptions< + ProviderDbContext> + _options; + + private SqliteTestDatabase( + SqliteConnection connection, + DbContextOptions< + ProviderDbContext> + options) + { + _connection = + connection; + + _options = + options; + } + + public static async Task< + SqliteTestDatabase> + CreateAsync( + CancellationToken + cancellationToken = + default) + { + var connection = + new SqliteConnection( + "Data Source=:memory:;Foreign Keys=True"); + + await connection + .OpenAsync( + cancellationToken); + + try + { + DbContextOptions< + ProviderDbContext> + options = + new DbContextOptionsBuilder< + ProviderDbContext>() + .UseSqlite( + connection) + .EnableDetailedErrors() + .Options; + + var database = + new SqliteTestDatabase( + connection, + options); + + await using + ProviderDbContext context = + database + .CreateContext(); + + await context.Database + .EnsureCreatedAsync( + cancellationToken); + + return database; + } + catch + { + await connection + .DisposeAsync(); + + throw; + } + } + + public ProviderDbContext + CreateContext() => + new( + _options); + + public ValueTask + DisposeAsync() => + _connection + .DisposeAsync(); +} \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Services/ProviderContrast.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Services/ProviderContrast.cs new file mode 100644 index 0000000..d4064c3 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/src/EfCoreProviderContrastMinimal/Services/ProviderContrast.cs @@ -0,0 +1,473 @@ +using EfCoreProviderContrastMinimal.Data; +using EfCoreProviderContrastMinimal.Models; +using EfCoreProviderContrastMinimal.Providers; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreProviderContrastMinimal.Services; + +public sealed record + ConstraintContrast( + bool InMemoryAccepted, + bool SqliteRejected); + +public sealed record + TransactionContrast( + bool InMemoryUnsupported, + bool SqliteRolledBack); + +public sealed record + CascadeContrast( + bool InMemoryOrphanRemains, + bool SqliteChildRemoved); + +public sealed record + ProviderContrastResult( + ConstraintContrast UniqueConstraint, + ConstraintContrast ForeignKey, + TransactionContrast Transaction, + CascadeContrast Cascade); + +public static class ProviderContrast +{ + public static async Task< + ConstraintContrast> + CheckUniqueConstraintAsync( + CancellationToken + cancellationToken = + default) + { + bool inMemoryAccepted; + + await using ( + var database = + new InMemoryTestDatabase()) + { + await using + ProviderDbContext context = + database + .CreateContext(); + + context.Projects.AddRange( + new Project + { + Id = 1, + Name = "Duplicate", + OwnerId = "owner" + }, + new Project + { + Id = 2, + Name = "Duplicate", + OwnerId = "owner" + }); + + await context + .SaveChangesAsync( + cancellationToken); + + inMemoryAccepted = + await context.Projects + .CountAsync( + cancellationToken) + == 2; + } + + bool sqliteRejected = + false; + + await using ( + SqliteTestDatabase database = + await SqliteTestDatabase + .CreateAsync( + cancellationToken)) + { + await using + ProviderDbContext context = + database + .CreateContext(); + + context.Projects.AddRange( + new Project + { + Id = 1, + Name = "Duplicate", + OwnerId = "owner" + }, + new Project + { + Id = 2, + Name = "Duplicate", + OwnerId = "owner" + }); + + try + { + await context + .SaveChangesAsync( + cancellationToken); + } + catch (DbUpdateException) + { + sqliteRejected = + true; + } + } + + return new ConstraintContrast( + InMemoryAccepted: + inMemoryAccepted, + + SqliteRejected: + sqliteRejected); + } + + public static async Task< + ConstraintContrast> + CheckForeignKeyAsync( + CancellationToken + cancellationToken = + default) + { + bool inMemoryAccepted; + + await using ( + var database = + new InMemoryTestDatabase()) + { + await using + ProviderDbContext context = + database + .CreateContext(); + + context.Tasks.Add( + new TaskItem + { + Id = 1, + Title = "Orphan", + ProjectId = 999 + }); + + await context + .SaveChangesAsync( + cancellationToken); + + inMemoryAccepted = + await context.Tasks + .AnyAsync( + cancellationToken); + } + + bool sqliteRejected = + false; + + await using ( + SqliteTestDatabase database = + await SqliteTestDatabase + .CreateAsync( + cancellationToken)) + { + await using + ProviderDbContext context = + database + .CreateContext(); + + context.Tasks.Add( + new TaskItem + { + Id = 1, + Title = "Orphan", + ProjectId = 999 + }); + + try + { + await context + .SaveChangesAsync( + cancellationToken); + } + catch (DbUpdateException) + { + sqliteRejected = + true; + } + } + + return new ConstraintContrast( + InMemoryAccepted: + inMemoryAccepted, + + SqliteRejected: + sqliteRejected); + } + + public static async Task< + TransactionContrast> + CheckTransactionAsync( + CancellationToken + cancellationToken = + default) + { + bool inMemoryUnsupported = + false; + + await using ( + var database = + new InMemoryTestDatabase()) + { + await using + ProviderDbContext context = + database + .CreateContext(); + + try + { + await context.Database + .BeginTransactionAsync( + cancellationToken); + } + catch (InvalidOperationException) + { + inMemoryUnsupported = + true; + } + } + + bool sqliteRolledBack; + + await using ( + SqliteTestDatabase database = + await SqliteTestDatabase + .CreateAsync( + cancellationToken)) + { + await using ( + ProviderDbContext write = + database + .CreateContext()) + { + await using + var transaction = + await write.Database + .BeginTransactionAsync( + cancellationToken); + + write.Projects.Add( + new Project + { + Id = 1, + Name = "Rolled Back", + OwnerId = "owner" + }); + + await write + .SaveChangesAsync( + cancellationToken); + + await transaction + .RollbackAsync( + cancellationToken); + } + + await using + ProviderDbContext verify = + database + .CreateContext(); + + sqliteRolledBack = + !await verify.Projects + .AnyAsync( + cancellationToken); + } + + return new TransactionContrast( + InMemoryUnsupported: + inMemoryUnsupported, + + SqliteRolledBack: + sqliteRolledBack); + } + + public static async Task< + CascadeContrast> + CheckCascadeAsync( + CancellationToken + cancellationToken = + default) + { + bool inMemoryOrphanRemains; + + await using ( + var database = + new InMemoryTestDatabase()) + { + await SeedCascadeGraphAsync( + database.CreateContext(), + cancellationToken); + + await using ( + ProviderDbContext delete = + database + .CreateContext()) + { + Project project = + await delete.Projects + .SingleAsync( + entity => + entity.Id + == 1, + cancellationToken); + + delete.Projects + .Remove( + project); + + await delete + .SaveChangesAsync( + cancellationToken); + } + + await using + ProviderDbContext verify = + database + .CreateContext(); + + inMemoryOrphanRemains = + await verify.Tasks + .AnyAsync( + task => + task.ProjectId + == 1, + cancellationToken); + } + + bool sqliteChildRemoved; + + await using ( + SqliteTestDatabase database = + await SqliteTestDatabase + .CreateAsync( + cancellationToken)) + { + await SeedCascadeGraphAsync( + database.CreateContext(), + cancellationToken); + + await using ( + ProviderDbContext delete = + database + .CreateContext()) + { + Project project = + await delete.Projects + .SingleAsync( + entity => + entity.Id + == 1, + cancellationToken); + + delete.Projects + .Remove( + project); + + await delete + .SaveChangesAsync( + cancellationToken); + } + + await using + ProviderDbContext verify = + database + .CreateContext(); + + sqliteChildRemoved = + !await verify.Tasks + .AnyAsync( + task => + task.ProjectId + == 1, + cancellationToken); + } + + return new CascadeContrast( + InMemoryOrphanRemains: + inMemoryOrphanRemains, + + SqliteChildRemoved: + sqliteChildRemoved); + } + + public static async Task< + ProviderContrastResult> + RunAsync( + CancellationToken + cancellationToken = + default) + { + ConstraintContrast + unique = + await CheckUniqueConstraintAsync( + cancellationToken); + + ConstraintContrast + foreignKey = + await CheckForeignKeyAsync( + cancellationToken); + + TransactionContrast + transaction = + await CheckTransactionAsync( + cancellationToken); + + CascadeContrast + cascade = + await CheckCascadeAsync( + cancellationToken); + + return new ProviderContrastResult( + UniqueConstraint: + unique, + + ForeignKey: + foreignKey, + + Transaction: + transaction, + + Cascade: + cascade); + } + + private static async Task + SeedCascadeGraphAsync( + ProviderDbContext context, + CancellationToken + cancellationToken) + { + await using ( + context) + { + var project = + new Project + { + Id = 1, + Name = "Cascade", + OwnerId = "owner" + }; + + project.Tasks.Add( + new TaskItem + { + Id = 1, + Title = "Child", + ProjectId = 1 + }); + + context.Projects.Add( + project); + + await context + .SaveChangesAsync( + cancellationToken); + } + } +} \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/EfCoreProviderContrastMinimal.Tests.csproj b/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/EfCoreProviderContrastMinimal.Tests.csproj new file mode 100644 index 0000000..7c787ff --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/EfCoreProviderContrastMinimal.Tests.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + enable + enable + true + false + true + Exe + + + + + + + + + all + + runtime; + build; + native; + contentfiles; + analyzers; + buildtransitive + + + + + + + + + + + + + \ No newline at end of file diff --git a/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/ProviderContrastTests.cs b/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/ProviderContrastTests.cs new file mode 100644 index 0000000..86673b2 --- /dev/null +++ b/ef-core/testing-sqlite-inmemory-vs-inmemory/tests/EfCoreProviderContrastMinimal.Tests/ProviderContrastTests.cs @@ -0,0 +1,119 @@ +using EfCoreProviderContrastMinimal.Services; + +namespace EfCoreProviderContrastMinimal.Tests; + +public sealed class + ProviderContrastTests +{ + [Fact] + public async Task + Sqlite_rejects_duplicate_unique_key() + { + ConstraintContrast result = + await ProviderContrast + .CheckUniqueConstraintAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.SqliteRejected); + } + + [Fact] + public async Task + InMemory_accepts_duplicate_unique_key() + { + ConstraintContrast result = + await ProviderContrast + .CheckUniqueConstraintAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.InMemoryAccepted); + } + + [Fact] + public async Task + Sqlite_rejects_orphan_foreign_key() + { + ConstraintContrast result = + await ProviderContrast + .CheckForeignKeyAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.SqliteRejected); + } + + [Fact] + public async Task + InMemory_accepts_orphan_foreign_key() + { + ConstraintContrast result = + await ProviderContrast + .CheckForeignKeyAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.InMemoryAccepted); + } + + [Fact] + public async Task + Sqlite_transaction_rolls_back() + { + TransactionContrast result = + await ProviderContrast + .CheckTransactionAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.SqliteRolledBack); + } + + [Fact] + public async Task + InMemory_transaction_is_unsupported_by_default() + { + TransactionContrast result = + await ProviderContrast + .CheckTransactionAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.InMemoryUnsupported); + } + + [Fact] + public async Task + Sqlite_database_cascade_removes_untracked_child() + { + CascadeContrast result = + await ProviderContrast + .CheckCascadeAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.SqliteChildRemoved); + } + + [Fact] + public async Task + InMemory_does_not_apply_database_cascade_to_untracked_child() + { + CascadeContrast result = + await ProviderContrast + .CheckCascadeAsync( + TestContext.Current + .CancellationToken); + + Assert.True( + result.InMemoryOrphanRemains); + } +} \ No newline at end of file