feat: spec + repo
This commit is contained in:
parent
eff887b49f
commit
2206fe027a
78 changed files with 3870 additions and 0 deletions
|
|
@ -12,5 +12,8 @@
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.SourceLink.Gitea" Version="10.0.102" />
|
<PackageVersion Include="Microsoft.SourceLink.Gitea" Version="10.0.102" />
|
||||||
<PackageVersion Include="TUnit" Version="1.11.51" />
|
<PackageVersion Include="TUnit" Version="1.11.51" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.6" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,8 @@
|
||||||
<Project Path="src/request.validation.tests/Geekeey.Request.Validation.Tests.csproj" />
|
<Project Path="src/request.validation.tests/Geekeey.Request.Validation.Tests.csproj" />
|
||||||
<Project Path="src/request.result/Geekeey.Request.Result.csproj" />
|
<Project Path="src/request.result/Geekeey.Request.Result.csproj" />
|
||||||
<Project Path="src/request.result.tests/Geekeey.Request.Result.Tests.csproj" />
|
<Project Path="src/request.result.tests/Geekeey.Request.Result.Tests.csproj" />
|
||||||
|
<Project Path="src/request.persistence/Geekeey.Request.Persistence.csproj" />
|
||||||
|
<Project Path="src/request.persistence.tests/Geekeey.Request.Persistence.Tests.csproj" />
|
||||||
|
<Project Path="src/request.persistence.entityframeworkcore/Geekeey.Request.Persistence.EntityFrameworkCore.csproj" />
|
||||||
|
<Project Path="src/request.persistence.entityframeworkcore.tests/Geekeey.Request.Persistence.EntityFrameworkCore.Tests.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[*.{cs,vb}]
|
||||||
|
dotnet_diagnostic.CA1822.severity = none
|
||||||
|
dotnet_diagnostic.CA1707.severity = none
|
||||||
|
dotnet_diagnostic.IDE0060.severity = none
|
||||||
|
dotnet_diagnostic.IDE0005.severity = none
|
||||||
|
dotnet_diagnostic.IDE0390.severity = none
|
||||||
|
dotnet_diagnostic.IDE0391.severity = none
|
||||||
|
|
@ -0,0 +1,507 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class EvaluationTests
|
||||||
|
{
|
||||||
|
private static async Task<FakeDbContext> CreateDbContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<FakeDbContext>()
|
||||||
|
.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
|
||||||
|
.EnableServiceProviderCaching(false)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
var context = new FakeDbContext(options);
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Repository<FakeEntity>> CreateRepo()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
return new Repository<FakeEntity>(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedData(Repository<FakeEntity> repo, params FakeEntity[] entities)
|
||||||
|
{
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_filters_entities_by_equality()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Gamma" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new NameEqualSpec("Beta"));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_multiple_filters_combine_as_and()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "AlphaBeta" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new MultipleWhereSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("AlphaBeta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_no_match_returns_empty_list()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new NameEqualSpec("NonExistent"));
|
||||||
|
|
||||||
|
await Assert.That(result).IsEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_with_FirstOrDefaultAsync_returns_matching()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var result = await repo.FirstOrDefaultAsync(new NameEqualSpec("Alpha"));
|
||||||
|
|
||||||
|
await Assert.That(result).IsNotNull();
|
||||||
|
await Assert.That(result!.Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_with_FirstOrDefaultAsync_no_match_returns_null()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var result = await repo.FirstOrDefaultAsync(new NameEqualSpec("NonExistent"));
|
||||||
|
|
||||||
|
await Assert.That(result).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_with_CountAsync_returns_correct_count()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var count = await repo.CountAsync(new NameEqualSpec("Alpha"));
|
||||||
|
|
||||||
|
await Assert.That(count).IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_with_AnyAsync_returns_true_when_match_exists()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync(new NameEqualSpec("Alpha"));
|
||||||
|
|
||||||
|
await Assert.That(any).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_with_AnyAsync_returns_false_when_no_match()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync(new NameEqualSpec("Beta"));
|
||||||
|
|
||||||
|
await Assert.That(any).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Search_filters_by_like_pattern()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Albatross" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new SearchSpec("Al%"));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Search_exact_match_works_without_wildcard()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Albatross" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new SearchSpec("Alpha"));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Search_multiple_terms_in_same_group_combine_as_or()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Gamma" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new MultiSearchSpec("Alpha", "Beta"));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task OrderBy_returns_ascending()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Charlie" },
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderByNameSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(3);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alpha");
|
||||||
|
await Assert.That(result[1].Name).IsEqualTo("Beta");
|
||||||
|
await Assert.That(result[2].Name).IsEqualTo("Charlie");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task OrderByDescending_returns_descending()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Charlie" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderByNameDescSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(3);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Charlie");
|
||||||
|
await Assert.That(result[1].Name).IsEqualTo("Beta");
|
||||||
|
await Assert.That(result[2].Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThenBy_chains_after_order_by()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
var id1 = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
var id2 = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||||
|
var id3 = Guid.Parse("00000000-0000-0000-0000-000000000003");
|
||||||
|
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Id = id2, Name = "Alpha" },
|
||||||
|
new FakeEntity { Id = id1, Name = "Alpha" },
|
||||||
|
new FakeEntity { Id = id3, Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderByNameThenByIdSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(3);
|
||||||
|
await Assert.That(result[0].Id).IsEqualTo(id1);
|
||||||
|
await Assert.That(result[1].Id).IsEqualTo(id2);
|
||||||
|
await Assert.That(result[2].Name).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ThenByDescending_chains_after_order_by()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
var id1 = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
var id2 = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||||
|
var id3 = Guid.Parse("00000000-0000-0000-0000-000000000003");
|
||||||
|
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Id = id2, Name = "Alpha" },
|
||||||
|
new FakeEntity { Id = id1, Name = "Alpha" },
|
||||||
|
new FakeEntity { Id = id3, Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderByNameThenByIdDescSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(3);
|
||||||
|
await Assert.That(result[0].Id).IsEqualTo(id2);
|
||||||
|
await Assert.That(result[1].Id).IsEqualTo(id1);
|
||||||
|
await Assert.That(result[2].Name).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Duplicate_order_chain_throws()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
await Assert.That(async () => await repo.ListAsync(new DuplicateOrderSpec()))
|
||||||
|
.Throws<DuplicateOrderChainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Take_limits_results()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Gamma" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderedTakeSpec(2));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alpha");
|
||||||
|
await Assert.That(result[1].Name).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Skip_skips_elements()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Gamma" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderedSkipSpec(1));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Skip_and_take_paginates_correctly()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Charlie" },
|
||||||
|
new FakeEntity { Name = "Delta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new OrderedSkipTakeSpec(1, 2));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Beta");
|
||||||
|
await Assert.That(result[1].Name).IsEqualTo("Charlie");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Skip_zero_returns_all()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new SkipOnlySpec(0));
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Pagination_is_ignored_by_CountAsync()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" },
|
||||||
|
new FakeEntity { Name = "Gamma" });
|
||||||
|
|
||||||
|
var count = await repo.CountAsync(new OrderedSkipTakeSpec(1, 1));
|
||||||
|
|
||||||
|
await Assert.That(count).IsEqualTo(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Pagination_is_ignored_by_AnyAsync()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync(new OrderedSkipSpec(10));
|
||||||
|
|
||||||
|
await Assert.That(any).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Select_projects_entity_to_new_type()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new NameProjectionSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
await Assert.That(result[0]).IsEqualTo("Alpha");
|
||||||
|
await Assert.That(result[1]).IsEqualTo("Beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Select_with_FirstOrDefaultAsync_returns_projected_result()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var result = await repo.FirstOrDefaultAsync(new NameProjectionSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task No_selector_on_projected_spec_throws()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
await Assert.That(async () => await repo.ListAsync(new NoSelectorSpec()))
|
||||||
|
.Throws<SelectorNotFoundException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Concurrent_selectors_throws()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
await Assert.That(async () => await repo.ListAsync(new ConcurrentSelectorSpec()))
|
||||||
|
.Throws<ConcurrentSelectorsException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_orderby_and_take_combine_correctly()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Charlie" },
|
||||||
|
new FakeEntity { Name = "Alice" },
|
||||||
|
new FakeEntity { Name = "Bob" },
|
||||||
|
new FakeEntity { Name = "David" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new CombinedWhereOrderTakeSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alice");
|
||||||
|
await Assert.That(result[1].Name).IsEqualTo("Bob");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PostProcessing_applies_after_query()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "AlphaBeta" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new PostProcessingSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Where_only_returns_filtered()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new WhereSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task FirstOrDefaultAsync_returns_first_matching()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var result = await repo.FirstOrDefaultAsync(new FirstOrderedSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).IsNotNull();
|
||||||
|
await Assert.That(result!.Name).IsEqualTo("Alpha");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CountAsync_without_spec_returns_all()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var count = await repo.CountAsync();
|
||||||
|
|
||||||
|
await Assert.That(count).IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AnyAsync_without_spec_returns_true_when_entities_exist()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo, new FakeEntity { Name = "Alpha" });
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync();
|
||||||
|
|
||||||
|
await Assert.That(any).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AnyAsync_without_spec_returns_false_when_empty()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync();
|
||||||
|
|
||||||
|
await Assert.That(any).IsFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AsAsyncEnumerable_streams_entities()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await SeedData(repo,
|
||||||
|
new FakeEntity { Name = "Alpha" },
|
||||||
|
new FakeEntity { Name = "Beta" });
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
await foreach (var _ in repo.AsAsyncEnumerable(new OrderByNameSpec()))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Assert.That(count).IsEqualTo(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="TUnit" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\request.persistence.entityframeworkcore\Geekeey.Request.Persistence.EntityFrameworkCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,403 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class IncludeEvaluatorTests
|
||||||
|
{
|
||||||
|
private static async Task<FakeDbContext> CreateDbContext()
|
||||||
|
{
|
||||||
|
var context = new FakeDbContext(new DbContextOptionsBuilder<FakeDbContext>()
|
||||||
|
.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
|
||||||
|
.EnableServiceProviderCaching(false)
|
||||||
|
.Options);
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Default evaluator (non-cached, reflection-based) ---
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_include_reference_nav()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "child",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new IncludeChildSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_include_collection_nav()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
};
|
||||||
|
entity.Details.Add(new FakeDetailEntity
|
||||||
|
{
|
||||||
|
Value = "detail",
|
||||||
|
});
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new IncludeDetailsSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_then_include_after_reference()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "child",
|
||||||
|
GrandChildren =
|
||||||
|
{
|
||||||
|
new FakeGrandChildEntity
|
||||||
|
{
|
||||||
|
Value = "grand",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
var result = await repo.ListAsync(new IncludeChildThenGrandChildrenSpec());
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_empty_includes()
|
||||||
|
{
|
||||||
|
var repo = await CreateRepo();
|
||||||
|
await repo.AddAsync(new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Alpha",
|
||||||
|
});
|
||||||
|
await repo.AddAsync(new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Beta",
|
||||||
|
});
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
var result = await repo.ListAsync(spec);
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_then_include_after_collection()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(dbContext);
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
};
|
||||||
|
entity.Details.Add(new FakeDetailEntity
|
||||||
|
{
|
||||||
|
Value = "detail",
|
||||||
|
DetailChildren =
|
||||||
|
{
|
||||||
|
new FakeDetailChildEntity
|
||||||
|
{
|
||||||
|
Value = "childDetail",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
Expression<Func<FakeDetailEntity, List<FakeDetailChildEntity>>> expr = static d => d.DetailChildren;
|
||||||
|
builder.IncludeExpressions.Add(new IncludeExpressionInfo(
|
||||||
|
expr, typeof(FakeEntity), typeof(List<FakeDetailChildEntity>), typeof(IEnumerable<FakeDetailEntity>)));
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
var result = await repo.ListAsync(spec);
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cached evaluator (delegate-based) ---
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_include_reference_nav()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "child",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dbContext.FakeEntities.Add(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Child);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(result[0].Name).IsEqualTo("Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_include_collection_nav()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
};
|
||||||
|
entity.Details.Add(new FakeDetailEntity
|
||||||
|
{
|
||||||
|
Value = "detail",
|
||||||
|
});
|
||||||
|
dbContext.FakeEntities.Add(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Details);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_then_include_after_reference()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "child",
|
||||||
|
GrandChildren =
|
||||||
|
{
|
||||||
|
new FakeGrandChildEntity
|
||||||
|
{
|
||||||
|
Value = "grand",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dbContext.FakeEntities.Add(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Child).ThenInclude(static c => c.GrandChildren);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_then_include_after_collection()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
var entity = new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
};
|
||||||
|
entity.Details.Add(new FakeDetailEntity
|
||||||
|
{
|
||||||
|
Value = "detail",
|
||||||
|
DetailChildren =
|
||||||
|
{
|
||||||
|
new FakeDetailChildEntity
|
||||||
|
{
|
||||||
|
Value = "childDetail",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dbContext.FakeEntities.Add(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
Expression<Func<FakeDetailEntity, List<FakeDetailChildEntity>>> expr = static d => d.DetailChildren;
|
||||||
|
builder.IncludeExpressions.Add(new IncludeExpressionInfo(
|
||||||
|
expr, typeof(FakeEntity), typeof(List<FakeDetailChildEntity>), typeof(IEnumerable<FakeDetailEntity>)));
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_empty_includes()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
dbContext.FakeEntities.AddRange(
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Alpha",
|
||||||
|
},
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Beta",
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comparison ---
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Default_and_cached_produce_same_results()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
dbContext.FakeEntities.Add(new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "child",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Child);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
var resultDefault = IncludeEvaluator.Default.Evaluate(query, spec).ToList();
|
||||||
|
var resultCached = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(resultDefault).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(resultCached).Count().IsEqualTo(1);
|
||||||
|
await Assert.That(resultDefault[0].Name).IsEqualTo(resultCached[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cache reuse ---
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_include_reuses_delegate()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
dbContext.FakeEntities.AddRange(
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "A",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "B",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "b",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Child);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
_ = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Cached_then_include_reuses_delegate()
|
||||||
|
{
|
||||||
|
var dbContext = await CreateDbContext();
|
||||||
|
var query = dbContext.FakeEntities.AsQueryable();
|
||||||
|
dbContext.FakeEntities.AddRange(
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "A",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "a",
|
||||||
|
GrandChildren =
|
||||||
|
{
|
||||||
|
new FakeGrandChildEntity
|
||||||
|
{
|
||||||
|
Value = "g1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new FakeEntity
|
||||||
|
{
|
||||||
|
Name = "B",
|
||||||
|
Child = new FakeChildEntity
|
||||||
|
{
|
||||||
|
Value = "b",
|
||||||
|
GrandChildren =
|
||||||
|
{
|
||||||
|
new FakeGrandChildEntity
|
||||||
|
{
|
||||||
|
Value = "g2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var builder = new SpecificationBuilder<FakeEntity>();
|
||||||
|
builder.Include(static e => e.Child).ThenInclude(static c => c.GrandChildren);
|
||||||
|
var spec = new IncludeOnlySpec(builder);
|
||||||
|
|
||||||
|
_ = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
var result = IncludeEvaluator.Cached.Evaluate(query, spec).ToList();
|
||||||
|
|
||||||
|
await Assert.That(result).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Repository<FakeEntity>> CreateRepo()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
return new Repository<FakeEntity>(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class RepositoryTests
|
||||||
|
{
|
||||||
|
private static async Task<FakeDbContext> CreateDbContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<FakeDbContext>()
|
||||||
|
.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
|
||||||
|
.EnableServiceProviderCaching(false)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
var context = new FakeDbContext(options);
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AddAsync_persists_entity()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
var entity = new FakeEntity { Name = "Test" };
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
await Assert.That(entity.Id).IsNotEqualTo(Guid.Empty);
|
||||||
|
await Assert.That(entity.Name).IsEqualTo("Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ListAsync_returns_all_entities()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
await repo.AddAsync(new FakeEntity { Name = "A" });
|
||||||
|
await repo.AddAsync(new FakeEntity { Name = "B" });
|
||||||
|
|
||||||
|
var list = await repo.ListAsync();
|
||||||
|
|
||||||
|
await Assert.That(list).Count().IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task UpdateAsync_modifies_entity()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
var entity = new FakeEntity { Name = "Original" };
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
|
||||||
|
entity.Name = "Updated";
|
||||||
|
await repo.UpdateAsync(entity);
|
||||||
|
|
||||||
|
var all = await repo.ListAsync();
|
||||||
|
var found = all.FirstOrDefault(e => e.Id == entity.Id);
|
||||||
|
await Assert.That(found).IsNotNull();
|
||||||
|
await Assert.That(found!.Name).IsEqualTo("Updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task DeleteAsync_removes_entity()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
var entity = new FakeEntity { Name = "Test" };
|
||||||
|
await repo.AddAsync(entity);
|
||||||
|
await repo.DeleteAsync(entity);
|
||||||
|
|
||||||
|
var all = await repo.ListAsync();
|
||||||
|
var found = all.FirstOrDefault(e => e.Id == entity.Id);
|
||||||
|
await Assert.That(found).IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CountAsync_returns_correct_count()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
await repo.AddAsync(new FakeEntity { Name = "A" });
|
||||||
|
await repo.AddAsync(new FakeEntity { Name = "B" });
|
||||||
|
|
||||||
|
var count = await repo.CountAsync();
|
||||||
|
|
||||||
|
await Assert.That(count).IsEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AnyAsync_returns_true_when_entities_exist()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
await repo.AddAsync(new FakeEntity { Name = "A" });
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync();
|
||||||
|
|
||||||
|
await Assert.That(any).IsTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AnyAsync_returns_false_when_no_entities()
|
||||||
|
{
|
||||||
|
var context = await CreateDbContext();
|
||||||
|
var repo = new Repository<FakeEntity>(context);
|
||||||
|
|
||||||
|
var any = await repo.AnyAsync();
|
||||||
|
|
||||||
|
await Assert.That(any).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class CombinedWhereOrderTakeSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public CombinedWhereOrderTakeSpec()
|
||||||
|
{
|
||||||
|
Query.Where(e => e.Name != "David")
|
||||||
|
.OrderBy(e => e.Name)
|
||||||
|
.Builder
|
||||||
|
.Take(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class ConcurrentSelectorSpec : Specification<FakeEntity, string>
|
||||||
|
{
|
||||||
|
public ConcurrentSelectorSpec()
|
||||||
|
{
|
||||||
|
Query.Select(e => e.Name);
|
||||||
|
Query.SelectMany(e => e.Name.Split(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class DuplicateOrderSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public DuplicateOrderSpec()
|
||||||
|
{
|
||||||
|
Query.OrderBy(e => e.Name);
|
||||||
|
Query.OrderBy(e => e.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeChildEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public Guid FakeEntityId { get; set; }
|
||||||
|
public FakeEntity FakeEntity { get; set; } = null!;
|
||||||
|
public List<FakeGrandChildEntity> GrandChildren { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeDbContext(DbContextOptions<FakeDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<FakeEntity> FakeEntities => Set<FakeEntity>();
|
||||||
|
public DbSet<FakeChildEntity> FakeChildEntities => Set<FakeChildEntity>();
|
||||||
|
public DbSet<FakeDetailEntity> FakeDetailEntities => Set<FakeDetailEntity>();
|
||||||
|
public DbSet<FakeGrandChildEntity> FakeGrandChildEntities => Set<FakeGrandChildEntity>();
|
||||||
|
public DbSet<FakeDetailChildEntity> FakeDetailChildEntities => Set<FakeDetailChildEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<FakeEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(static e => e.Id);
|
||||||
|
entity.Property(static e => e.Name).IsRequired();
|
||||||
|
entity.HasOne(static e => e.Child)
|
||||||
|
.WithOne(static c => c.FakeEntity)
|
||||||
|
.HasForeignKey<FakeChildEntity>(static c => c.FakeEntityId);
|
||||||
|
entity.HasMany(static e => e.Details)
|
||||||
|
.WithOne(static d => d.FakeEntity)
|
||||||
|
.HasForeignKey(static d => d.FakeEntityId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FakeChildEntity>(child =>
|
||||||
|
{
|
||||||
|
child.HasKey(static c => c.Id);
|
||||||
|
child.Property(static c => c.Value).IsRequired();
|
||||||
|
child.HasMany(static c => c.GrandChildren)
|
||||||
|
.WithOne(static g => g.FakeChildEntity)
|
||||||
|
.HasForeignKey(static g => g.FakeChildEntityId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FakeGrandChildEntity>(grandChild =>
|
||||||
|
{
|
||||||
|
grandChild.HasKey(static g => g.Id);
|
||||||
|
grandChild.Property(static g => g.Value).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FakeDetailEntity>(detail =>
|
||||||
|
{
|
||||||
|
detail.HasKey(static d => d.Id);
|
||||||
|
detail.Property(static d => d.Value).IsRequired();
|
||||||
|
detail.HasMany(static d => d.DetailChildren)
|
||||||
|
.WithOne(static dc => dc.FakeDetailEntity)
|
||||||
|
.HasForeignKey(static dc => dc.FakeDetailEntityId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FakeDetailChildEntity>(detailChild =>
|
||||||
|
{
|
||||||
|
detailChild.HasKey(static d => d.Id);
|
||||||
|
detailChild.Property(static d => d.Value).IsRequired();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeDetailChildEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public Guid FakeDetailEntityId { get; set; }
|
||||||
|
public FakeDetailEntity FakeDetailEntity { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeDetailEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public Guid FakeEntityId { get; set; }
|
||||||
|
public FakeEntity FakeEntity { get; set; } = null!;
|
||||||
|
public List<FakeDetailChildEntity> DetailChildren { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public FakeChildEntity Child { get; set; } = null!;
|
||||||
|
public List<FakeDetailEntity> Details { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FakeGrandChildEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public Guid FakeChildEntityId { get; set; }
|
||||||
|
public FakeChildEntity FakeChildEntity { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class FirstOrderedSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public FirstOrderedSpec()
|
||||||
|
{
|
||||||
|
Query.OrderBy(e => e.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class IncludeChildSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public IncludeChildSpec()
|
||||||
|
{
|
||||||
|
Query.Include(static e => e.Child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class IncludeChildThenGrandChildrenSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public IncludeChildThenGrandChildrenSpec()
|
||||||
|
{
|
||||||
|
Query.Include(static e => e.Child).ThenInclude(static c => c.GrandChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Persistence.EntityFrameworkCore.Tests;
|
||||||
|
|
||||||
|
internal sealed class IncludeDetailsSpec : Specification<FakeEntity>
|
||||||
|
{
|
||||||
|
public IncludeDetailsSpec()
|
||||||
|
{
|
||||||
|
Query.Include(static e => e.Details);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue