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
18 changes: 18 additions & 0 deletions src/QuerySpecification.EntityFrameworkCore/RepositoryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ public virtual async Task<List<TResult>> ToListAsync<TResult>(Specification<T, T
return await GenerateQuery(specification).ToListAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task<Dictionary<TKey, T>> ToDictionaryAsync<TKey>(Func<T, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull
{
return await _dbContext.Set<T>().ToDictionaryAsync(keySelector, cancellationToken);
}

/// <inheritdoc/>
public async Task<Dictionary<TKey, T>> ToDictionaryAsync<TKey>(Specification<T> specification, Func<T, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull
{
return await GenerateQuery(specification).ToDictionaryAsync(keySelector, cancellationToken);
}

/// <inheritdoc/>
public async Task<Dictionary<TKey, TResult>> ToDictionaryAsync<TResult, TKey>(Specification<T, TResult> specification, Func<TResult, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull
{
return await GenerateQuery(specification).ToDictionaryAsync(keySelector, cancellationToken);
}

/// <inheritdoc/>
public virtual async Task<int> CountAsync(CancellationToken cancellationToken = default)
{
Expand Down
10 changes: 10 additions & 0 deletions src/QuerySpecification.EntityFrameworkCore/RepositoryWithMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,14 @@ public virtual async Task<PagedResult<TResult>> ProjectToListAsync<TResult>(Spec

return new PagedResult<TResult>(data, pagination);
}

/// <inheritdoc/>
public virtual async Task<Dictionary<TKey, TResult>> ProjectToDictionaryAsync<TResult, TKey>(Specification<T> specification, Func<TResult, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull
{
var query = GenerateQuery(specification).AsNoTracking();

var projectedQuery = Map<TResult>(query);

return await projectedQuery.ToDictionaryAsync(keySelector, cancellationToken);
}
}
12 changes: 12 additions & 0 deletions src/QuerySpecification/IProjectionRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,16 @@ public interface IProjectionRepository<T> where T : class
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the paged list of projected results.</returns>
Task<PagedResult<TResult>> ProjectToListAsync<TResult>(Specification<T> specification, IPagingFilter filter, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates a dictionary from the projected elements that satisfy the specification, using the provided key selector function.
/// It ignores the selector in the specification, and projects the entities to the result type using the Map method.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the resulting dictionary. Must be non-nullable.</typeparam>
/// <typeparam name="TResult">The type of the projected element for each entity.</typeparam>
/// <param name="specification">The specification to evaluate.</param>
/// <param name="keySelector">A function to extract a key from each projected element.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary mapping each key to its projected element.</returns>
Task<Dictionary<TKey, TResult>> ProjectToDictionaryAsync<TResult, TKey>(Specification<T> specification, Func<TResult, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull;
}
31 changes: 31 additions & 0 deletions src/QuerySpecification/IReadRepositoryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,37 @@ public interface IReadRepositoryBase<T> where T : class
/// <returns>A task that represents the asynchronous operation. The task result contains the list of entities that match the specification and are projected to a result.</returns>
Task<List<TResult>> ToListAsync<TResult>(Specification<T, TResult> specification, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously creates a dictionary from the entities, using the specified key selector function.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the resulting dictionary. Must be a non-nullable type.</typeparam>
/// <param name="keySelector">A function to extract a key from each element.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary mapping each key to its corresponding entity.</returns>
Task<Dictionary<TKey, T>> ToDictionaryAsync<TKey>(Func<T, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull;

/// <summary>
/// Asynchronously creates a dictionary from the entities that satisfy the specification, using the provided key selector function.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the resulting dictionary. Must be non-nullable.</typeparam>
/// <param name="specification">The specification to evaluate.</param>
/// <param name="keySelector">A function to extract a key from each element.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary mapping each key to its corresponding entity.</returns>
Task<Dictionary<TKey, T>> ToDictionaryAsync<TKey>(Specification<T> specification, Func<T, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull;

/// <summary>
/// Asynchronously creates a dictionary from the projected elements that satisfy the specification, using the provided key selector function.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the resulting dictionary. Must be non-nullable.</typeparam>
/// <typeparam name="TResult">The type of the projected element for each entity.</typeparam>
/// <param name="specification">The specification to evaluate.</param>
/// <param name="keySelector">A function to extract a key from each projected element.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary mapping each key to its projected element.</returns>
Task<Dictionary<TKey, TResult>> ToDictionaryAsync<TResult, TKey>(Specification<T, TResult> specification, Func<TResult, TKey> keySelector, CancellationToken cancellationToken = default) where TKey : notnull;


/// <summary>
/// Gets the count of all entities.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,54 @@ await SeedRangeAsync<Country>(
spec.Skip.Should().Be(1);
spec.Take.Should().Be(2);
}

[Fact]
public async Task ProjectToDictionaryAsync_ReturnsProjectedItems_GivenSpec()
{
var expected = new Dictionary<int, CountryDto>
{
[1] = new(1, "b"),
[2] = new(2, "b"),
[3] = new(3, "b"),
};
await SeedRangeAsync<Country>(
[
new() { No = 9, Name = "a" },
new() { No = 9, Name = "c" },
new() { No = 1, Name = "b" },
new() { No = 2, Name = "b" },
new() { No = 3, Name = "b" },
new() { No = 9, Name = "d" },
]);

var repo = new Repository<Country>(DbContext);
var spec = new Specification<Country>();
spec.Query
.Where(x => x.Name == "b");

var result = await repo.ProjectToDictionaryAsync<CountryDto, int>(spec, x => x.No);

result.Should().HaveSameCount(expected);
result.Should().BeEquivalentTo(expected);
}

[Fact]
public async Task ProjectToDictionaryAsync_ReturnsEmptyDictionary_GivenSpecWithNoMatch()
{
await SeedRangeAsync<Country>(
[
new() { No = 9, Name = "a" },
new() { No = 9, Name = "c" },
new() { No = 9, Name = "d" },
]);

var repo = new Repository<Country>(DbContext);
var spec = new Specification<Country>();
spec.Query
.Where(x => x.Name == "x");

var result = await repo.ProjectToDictionaryAsync<CountryDto, int>(spec, x => x.No);

result.Should().BeEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace Tests.Repositories;

[Collection("SharedCollection")]
public class Repository_ToDictionaryTests(TestFactory factory) : IntegrationTest(factory)
{
public record CountryDto(int Id, string? Name);

[Fact]
public async Task ToDictionaryAsync_ReturnsAllItems()
{
var expected = new List<Country>
{
new() { Name = "a" },
new() { Name = "b" },
new() { Name = "c" },
};
await SeedRangeAsync(expected);

var repo = new Repository<Country>(DbContext);

var result = await repo.ToDictionaryAsync(x => x.Id);

result.Should().HaveSameCount(expected);
result.Values.Should().BeEquivalentTo(expected);
}

[Fact]
public async Task ToDictionaryAsync_ReturnsFilteredItems_GivenSpec()
{
var expected = new List<Country>
{
new() { Name = "b" },
new() { Name = "b" },
new() { Name = "b" },
};
await SeedRangeAsync(
[
new() { Name = "a" },
new() { Name = "c" },
.. expected,
new() { Name = "d" },
]);

var repo = new Repository<Country>(DbContext);
var spec = new Specification<Country>();
spec.Query
.Where(x => x.Name == "b");

var result = await repo.ToDictionaryAsync(spec, x => x.Id);

result.Should().HaveSameCount(expected);
result.Values.Should().BeEquivalentTo(expected);
}

[Fact]
public async Task ToDictionaryAsync_ReturnsFilteredItems_GivenProjectionSpec()
{
var seeded = new List<Country>
{
new() { Name = "b" },
new() { Name = "b" },
new() { Name = "b" },
};
await SeedRangeAsync<Country>(
[
new() { Name = "a" },
new() { Name = "c" },
.. seeded,
new() { Name = "d" },
]);

var repo = new Repository<Country>(DbContext);
var spec = new Specification<Country, CountryDto>();
spec.Query
.Where(x => x.Name == "b")
.Select(x => new CountryDto(x.Id, x.Name));

var result = await repo.ToDictionaryAsync(spec, x => x.Id);

result.Should().HaveCount(seeded.Count);
result.Values.Should().AllSatisfy(dto => dto.Name.Should().Be("b"));
}
}
Loading