diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/Cnblogs.Architecture.Ddd.Cqrs.Abstractions.csproj b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/Cnblogs.Architecture.Ddd.Cqrs.Abstractions.csproj
index 14aeb37..ee6a8bb 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/Cnblogs.Architecture.Ddd.Cqrs.Abstractions.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/Cnblogs.Architecture.Ddd.Cqrs.Abstractions.csproj
@@ -17,8 +17,8 @@
-
-
+
+
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/PageableQueryHandlerBase.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/PageableQueryHandlerBase.cs
index 9a9ee57..83fc8b5 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/PageableQueryHandlerBase.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/PageableQueryHandlerBase.cs
@@ -1,5 +1,4 @@
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
-
using Mapster;
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
@@ -21,15 +20,21 @@ public async Task> Handle(TQuery request, CancellationToken can
request.OrderByString,
out var orderBySegments);
+ var isNegativeIndexing = request.PagingParams?.PageIndex < 0;
+ if (isNegativeIndexing && orderBySegments is { Count: > 0 })
+ {
+ orderBySegments = orderBySegments.Select(o => o with { IsDesc = !o.IsDesc }).ToList();
+ }
+
var ordered = hasOrderBy && orderBySegments is { Count: > 0 }
? queryable.OrderBy(orderBySegments)
- : DefaultOrderBy(request, queryable);
+ : DefaultOrderBy(request, queryable, isNegativeIndexing);
var totalCount = 0;
if (request.PagingParams != null)
{
totalCount = await CountAsync(request, queryable);
- if (request.PagingParams.PageSize == 0 || totalCount == 0)
+ if (request.PagingParams.PageSize <= 0 || totalCount == 0)
{
// need count only or no available item, short circuit here.
return new PagedList([], request.PagingParams, totalCount);
@@ -60,6 +65,17 @@ public async Task> Handle(TQuery request, CancellationToken can
/// Ordered .
protected abstract IQueryable DefaultOrderBy(TQuery query, IQueryable queryable);
+ ///
+ /// The default reverse order by field, used when is not present and pageIndex is negative.
+ ///
+ /// The query parameters.
+ /// returned by .
+ /// Ordered .
+ protected virtual IQueryable DefaultReverseOrderBy(TQuery query, IQueryable queryable)
+ {
+ return DefaultOrderBy(query, queryable);
+ }
+
///
/// Create queryable and apply filter, return filtered .
///
@@ -95,4 +111,12 @@ protected virtual IQueryable ProjectToView(TQuery query, IQueryableProjected .
/// The query result.
protected abstract Task> ToListAsync(TQuery query, IQueryable queryable);
+
+ private IQueryable DefaultOrderBy(
+ TQuery query,
+ IQueryable queryable,
+ bool isNegativeIndexing)
+ {
+ return isNegativeIndexing ? DefaultReverseOrderBy(query, queryable) : DefaultOrderBy(query, queryable);
+ }
}
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/PagingParamsModelBinder.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/PagingParamsModelBinder.cs
index df11d82..de79dc7 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/PagingParamsModelBinder.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/PagingParamsModelBinder.cs
@@ -29,9 +29,9 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
}
var pageIndexSuccess = int.TryParse(pageIndexString, out var pageIndexNumber);
- if (pageIndexSuccess == false || pageIndexNumber <= 0)
+ if (pageIndexSuccess == false)
{
- bindingContext.ModelState.TryAddModelError(modelName, "PageIndex must be a positive number");
+ bindingContext.ModelState.TryAddModelError(modelName, "PageIndex must be a number");
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
@@ -39,7 +39,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
var pageSizeSuccess = int.TryParse(pageSizeString, out var pageSizeNumber);
if (pageSizeSuccess == false || pageSizeNumber < 0)
{
- bindingContext.ModelState.TryAddModelError(modelName, "PageIndex must be a positive number or 0");
+ bindingContext.ModelState.TryAddModelError(modelName, "PageSize must be a positive number or 0");
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj
index 8168f94..99e1d98 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj
@@ -12,11 +12,11 @@
-
+
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/PagingParams.cs b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/PagingParams.cs
index e505c45..f2c8a6f 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/PagingParams.cs
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/PagingParams.cs
@@ -1,4 +1,4 @@
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@@ -10,7 +10,7 @@ namespace Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
///
/// 页码。
/// 每页元素数。
-public record PagingParams([Range(1, int.MaxValue)] int PageIndex, [Range(0, int.MaxValue)] int PageSize)
+public record PagingParams(int PageIndex, [Range(0, int.MaxValue)] int PageSize)
{
///
public override string ToString()
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryPager.cs b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryPager.cs
index d1949aa..00b5ba7 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryPager.cs
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryPager.cs
@@ -18,7 +18,7 @@ public static class QueryPager
public static IQueryable Paging(this IQueryable queryable, PagingParams pagingParams)
{
var (pageIndex, pageSize) = pagingParams;
- return queryable.Paging(pageIndex, pageSize);
+ return queryable.Paging(pageIndex < 0 ? -pageIndex : pageIndex, pageSize < 0 ? -pageSize : pageSize);
}
///
@@ -53,4 +53,4 @@ public static IQueryable Paging(this IQueryable queryable, int pageInde
? queryable.Take(pageSize)
: queryable.Skip((pageIndex - 1) * pageSize).Take(pageSize);
}
-}
\ No newline at end of file
+}
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryStringBuilder.cs b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryStringBuilder.cs
index c5b2c80..c5b0908 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryStringBuilder.cs
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.Abstractions/QueryStringBuilder.cs
@@ -45,15 +45,15 @@ public QueryStringBuilder(
///
/// 分页参数。
///
- public QueryStringBuilder AddPaging(PagingParams pagingParams)
- => AddPaging(pagingParams.PageIndex, pagingParams.PageSize);
+ public QueryStringBuilder AddPaging(PagingParams? pagingParams)
+ => AddPaging(pagingParams?.PageIndex, pagingParams?.PageSize);
///
/// 添加分页参数。
///
/// 页码。
/// 分页大小。
- public QueryStringBuilder AddPaging(int pageIndex, int pageSize)
+ public QueryStringBuilder AddPaging(int? pageIndex, int? pageSize)
=> Add(nameof(pageIndex), pageIndex).Add(nameof(pageSize), pageSize);
///
@@ -170,4 +170,4 @@ public override string ToString()
{
return Build();
}
-}
\ No newline at end of file
+}
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory.csproj
index 67631a4..ef7c884 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.InMemory.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis.csproj
index 756e73b..cde4f08 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis/Cnblogs.Architecture.Ddd.Infrastructure.CacheProviders.Redis.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.csproj
index 33baa71..6ac3d2d 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj
index 941b4b6..c3fe132 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb.csproj
index 18fdb2f..0a8dd80 100644
--- a/src/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb.csproj
+++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb/Cnblogs.Architecture.Ddd.Infrastructure.MongoDb.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj b/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj
index 535e7de..abe9d12 100644
--- a/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj
+++ b/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj
@@ -1,7 +1,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -12,7 +12,7 @@
-
+
diff --git a/test/Cnblogs.Architecture.IntegrationTests/CustomModelBinderTests.cs b/test/Cnblogs.Architecture.IntegrationTests/CustomModelBinderTests.cs
index b647e8d..f00288d 100644
--- a/test/Cnblogs.Architecture.IntegrationTests/CustomModelBinderTests.cs
+++ b/test/Cnblogs.Architecture.IntegrationTests/CustomModelBinderTests.cs
@@ -41,7 +41,6 @@ public async Task PagingParamsModelBinder_NoPageIndexOrPageSize_NullAsync(string
[Theory]
[InlineData("hello")]
- [InlineData("-1")]
public async Task PagingParamsModelBinder_PageIndexInvalid_FailAsync(string pageIndex)
{
// Arrange
diff --git a/test/Cnblogs.Architecture.TestShared/EntityGenerator.Varies.cs b/test/Cnblogs.Architecture.TestShared/EntityGenerator.Varies.cs
index 84daed5..96d357f 100644
--- a/test/Cnblogs.Architecture.TestShared/EntityGenerator.Varies.cs
+++ b/test/Cnblogs.Architecture.TestShared/EntityGenerator.Varies.cs
@@ -44,6 +44,15 @@ public EntityGenerator VaryByDateTimeDay(
days,
DateTime.Now);
+ public EntityGenerator VaryByDateTimeDay(
+ Expression>? datetimeAccess,
+ int days)
+ => VaryByDateTime(
+ datetimeAccess,
+ (start, day) => start.AddDays(-day),
+ days,
+ DateTime.Now);
+
public EntityGenerator VaryByDateTime(
Expression>? datetimeAccess,
Func timeDiffer,
@@ -59,6 +68,21 @@ public EntityGenerator VaryByDateTime(
return VaryBy(datetimeAccess, dates);
}
+ public EntityGenerator VaryByDateTime(
+ Expression>? datetimeAccess,
+ Func timeDiffer,
+ int diffs,
+ DateTime startDate)
+ {
+ var dates = new DateTimeOffset[diffs];
+ for (var i = 0; i < diffs; i++)
+ {
+ dates[i] = timeDiffer(startDate, i);
+ }
+
+ return VaryBy(datetimeAccess, dates);
+ }
+
public EntityGenerator VaryByBoolean(Expression>? booleanAccess)
=> VaryBy(booleanAccess, true, false);
@@ -67,4 +91,4 @@ public EntityGenerator VaryByInt(Expression>? intAcc
public EntityGenerator MultiplyBy(int times)
=> VaryBy(null, Enumerable.Range(0, times));
-}
\ No newline at end of file
+}
diff --git a/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj b/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj
index 50d1a3c..b9b3ec6 100644
--- a/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj
+++ b/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj
@@ -2,8 +2,8 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/FakePostDto.cs b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/FakePostDto.cs
new file mode 100644
index 0000000..10e1a96
--- /dev/null
+++ b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/FakePostDto.cs
@@ -0,0 +1,3 @@
+namespace Cnblogs.Architecture.UnitTests.Cqrs.FakeObjects;
+
+public record FakePostDto(int Id, DateTimeOffset DateAdded, DateTimeOffset DateUpdated);
diff --git a/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQuery.cs b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQuery.cs
new file mode 100644
index 0000000..4b81b78
--- /dev/null
+++ b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQuery.cs
@@ -0,0 +1,7 @@
+using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
+using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
+
+namespace Cnblogs.Architecture.UnitTests.Cqrs.FakeObjects;
+
+public record TestPageableQuery(bool? Deleted, PagingParams? PagingParams, string? OrderByString)
+ : IPageableQuery;
diff --git a/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQueryHandler.cs b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQueryHandler.cs
new file mode 100644
index 0000000..8ac099d
--- /dev/null
+++ b/test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/TestPageableQueryHandler.cs
@@ -0,0 +1,33 @@
+using Cnblogs.Architecture.Ddd.Cqrs.EntityFramework;
+using Cnblogs.Architecture.UnitTests.Infrastructure.FakeObjects;
+using Microsoft.EntityFrameworkCore;
+
+namespace Cnblogs.Architecture.UnitTests.Cqrs.FakeObjects;
+
+public class TestPageableQueryHandler(DbContext context)
+ : EfPageableQueryHandler
+{
+ ///
+ protected override IQueryable DefaultOrderBy(TestPageableQuery query, IQueryable queryable)
+ {
+ return queryable.OrderByDescending(x => x.DateAdded).ThenByDescending(x => x.DateUpdated);
+ }
+
+ ///
+ protected override IQueryable DefaultReverseOrderBy(TestPageableQuery query, IQueryable queryable)
+ {
+ return queryable.OrderBy(x => x.DateAdded).ThenBy(x => x.DateUpdated);
+ }
+
+ ///
+ protected override IQueryable Filter(TestPageableQuery query)
+ {
+ var queryable = context.Set().AsNoTracking();
+ if (query.Deleted.HasValue)
+ {
+ queryable = queryable.Where(x => x.Deleted == query.Deleted.Value);
+ }
+
+ return queryable;
+ }
+}
diff --git a/test/Cnblogs.Architecture.UnitTests/Cqrs/Handlers/PageableQueryHandlerTests.cs b/test/Cnblogs.Architecture.UnitTests/Cqrs/Handlers/PageableQueryHandlerTests.cs
new file mode 100644
index 0000000..bcadae2
--- /dev/null
+++ b/test/Cnblogs.Architecture.UnitTests/Cqrs/Handlers/PageableQueryHandlerTests.cs
@@ -0,0 +1,253 @@
+using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
+using Cnblogs.Architecture.TestShared;
+using Cnblogs.Architecture.UnitTests.Cqrs.FakeObjects;
+using Cnblogs.Architecture.UnitTests.Infrastructure.FakeObjects;
+using Microsoft.EntityFrameworkCore;
+
+namespace Cnblogs.Architecture.UnitTests.Cqrs.Handlers;
+
+public class PageableQueryHandlerTests
+{
+ [Fact]
+ public async Task Handle_NoPaging_AllItemsAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ var response = await handler.Handle(new TestPageableQuery(null, null, null), CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, response.TotalCount);
+ Assert.Equal(1, response.PageIndex);
+ Assert.Equal(posts.Count, response.PageSize);
+ Assert.Equal(posts.Count, response.Items.Count);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public async Task Handle_ZeroOrNegativePageSize_EmptyListWithTotalCountAsync(int pageSize)
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = 1;
+ var response = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), null),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, response.TotalCount);
+ Assert.Equal(pageIndex, response.PageIndex);
+ Assert.Equal(pageSize, response.PageSize);
+ Assert.Empty(response.Items);
+ }
+
+ [Fact]
+ public async Task Handle_Paging_PagedItemsWithTotalCountAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ var newestPostId = posts.OrderByDescending(x => x.DateAdded).ThenByDescending(x => x.DateUpdated).First().Id;
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = 1;
+ const int pageSize = 3;
+ var response = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), null),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, response.TotalCount);
+ Assert.Equal(pageIndex, response.PageIndex);
+ Assert.Equal(pageSize, response.PageSize);
+ Assert.Equal(pageSize, response.Items.Count);
+ Assert.Equal(newestPostId, response.Items.First().Id);
+ }
+
+ [Fact]
+ public async Task Handle_NegativeIndexing_PagedItemsWithTotalCountAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ var oldestPostId = posts.OrderBy(x => x.DateAdded).ThenBy(x => x.DateUpdated).First().Id;
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = -1;
+ const int pageSize = 3;
+ var response = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), null),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, response.TotalCount);
+ Assert.Equal(pageIndex, response.PageIndex);
+ Assert.Equal(pageSize, response.PageSize);
+ Assert.Equal(pageSize, response.Items.Count);
+ Assert.Equal(oldestPostId, response.Items.First().Id);
+ }
+
+ [Fact]
+ public async Task Handle_OrderByString_OverrideDefaultOrderByAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ OrderBySegmentConfig.RegisterSortableProperty("id", x => x.Id);
+ var minId = posts.Min(x => x.Id);
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = 1;
+ const int pageSize = 3;
+ var response = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), "id"),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, response.TotalCount);
+ Assert.Equal(pageIndex, response.PageIndex);
+ Assert.Equal(pageSize, response.PageSize);
+ Assert.Equal(pageSize, response.Items.Count);
+ Assert.Equal(minId, response.Items.First().Id);
+ }
+
+ [Fact]
+ public async Task Handle_OrderByStringWithNegativeIndexing_OverrideDefaultOrderByAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ OrderBySegmentConfig.RegisterSortableProperty("id", x => x.Id);
+ var maxId = posts.Max(x => x.Id);
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = -1;
+ const int pageSize = 3;
+ var negativeIndexing = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), "id"),
+ CancellationToken.None);
+ var descendingOrderBy = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(-pageIndex, pageSize), "-id"),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, negativeIndexing.TotalCount);
+ Assert.Equal(pageIndex, negativeIndexing.PageIndex);
+ Assert.Equal(pageSize, negativeIndexing.PageSize);
+ Assert.Equal(pageSize, negativeIndexing.Items.Count);
+ Assert.Equal(maxId, negativeIndexing.Items.First().Id);
+ Assert.Equivalent(negativeIndexing.Items, descendingOrderBy.Items);
+ }
+
+ [Fact]
+ public async Task Handle_MultipleOrderBySegment_OverrideDefaultOrderByAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ OrderBySegmentConfig.RegisterSortableProperty("dateAdded", x => x.DateAdded);
+ OrderBySegmentConfig.RegisterSortableProperty("dateUpdated", x => x.DateUpdated);
+ var newestPostId = posts.OrderByDescending(x => x.DateAdded).ThenByDescending(x => x.DateUpdated).First().Id;
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = 1;
+ const int pageSize = 3;
+ var withOrderByString = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), "-dateAdded,-dateUpdated"),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, withOrderByString.TotalCount);
+ Assert.Equal(pageIndex, withOrderByString.PageIndex);
+ Assert.Equal(pageSize, withOrderByString.PageSize);
+ Assert.Equal(pageSize, withOrderByString.Items.Count);
+ Assert.Equal(newestPostId, withOrderByString.Items.First().Id);
+ }
+
+ [Fact]
+ public async Task Handle_MultipleOrderBySegmentWithNegativeIndexing_EquivlentToNelegateOrderByStringAsync()
+ {
+ // Arrange
+ var posts = new EntityGenerator(new FakePost())
+ .VaryByDateTimeDay(x => x.DateAdded, 5)
+ .VaryByDateTimeDay(x => x.DateUpdated, 2)
+ .FillWithInt(x => x.Id, 1, 100)
+ .Generate();
+ OrderBySegmentConfig.RegisterSortableProperty("dateAdded", x => x.DateAdded);
+ OrderBySegmentConfig.RegisterSortableProperty("dateUpdated", x => x.DateUpdated);
+ var oldestPostId = posts.OrderBy(x => x.DateAdded).ThenBy(x => x.DateUpdated).First().Id;
+ var dbContext = await GetDbContextAsync(posts);
+ var handler = new TestPageableQueryHandler(dbContext);
+
+ // Act
+ const int pageIndex = -1;
+ const int pageSize = 3;
+ var negativeIndexing = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(pageIndex, pageSize), "-dateAdded,-dateUpdated"),
+ CancellationToken.None);
+ var negativeOrdering = await handler.Handle(
+ new TestPageableQuery(null, new PagingParams(-pageIndex, pageSize), "dateAdded,dateUpdated"),
+ CancellationToken.None);
+
+ // Assert
+ Assert.Equal(posts.Count, negativeIndexing.TotalCount);
+ Assert.Equal(pageIndex, negativeIndexing.PageIndex);
+ Assert.Equal(pageSize, negativeIndexing.PageSize);
+ Assert.Equal(pageSize, negativeIndexing.Items.Count);
+ Assert.Equal(oldestPostId, negativeIndexing.Items.First().Id);
+ Assert.Equivalent(negativeIndexing.Items, negativeOrdering.Items);
+ }
+
+ private static async Task GetDbContextAsync(ICollection entities)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(nameof(PageableQueryHandlerTests)).Options;
+ var context = new FakeDbContext(options);
+ await context.Database.EnsureDeletedAsync();
+ await context.Database.EnsureCreatedAsync();
+ await context.AddRangeAsync(entities);
+ await context.SaveChangesAsync();
+ return context;
+ }
+}
diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs
index 703dc68..6b506df 100644
--- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs
+++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs
@@ -22,8 +22,7 @@ public async Task GetEntityAsync_Include_GetEntityAsync()
new EntityGenerator(new FakePost())
.Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var repository = new TestRepository(Substitute.For(), db);
@@ -36,6 +35,16 @@ public async Task GetEntityAsync_Include_GetEntityAsync()
Assert.Equivalent(entity.Posts, got.Posts);
}
+ private static FakeDbContext GetFakeDbContext()
+ {
+ var options = new DbContextOptionsBuilder().UseInMemoryDatabase(nameof(BaseRepositoryTests))
+ .Options;
+ var context = new FakeDbContext(options);
+ context.Database.EnsureDeleted();
+ context.Database.EnsureCreated();
+ return context;
+ }
+
[Fact]
public async Task GetEntityAsync_StringBasedInclude_NotNullAsync()
{
@@ -48,8 +57,7 @@ public async Task GetEntityAsync_StringBasedInclude_NotNullAsync()
new EntityGenerator(new FakePost())
.Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var repository = new TestRepository(Substitute.For(), db);
@@ -75,8 +83,7 @@ public async Task GetEntityAsync_ThenInclude_NotNullAsync()
.HasManyForEachEntity(x => x.Tags, new EntityGenerator(new FakeTag()))
.Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var repository = new TestRepository(Substitute.For(), db);
@@ -101,8 +108,7 @@ public async Task SaveEntitiesAsync_CallBeforeUpdateForRelatedEntity_UpdateDateU
new EntityGenerator(new FakePost())
.Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var repository = new TestRepository(Substitute.For(), db);
@@ -126,11 +132,10 @@ public async Task SaveEntitiesAsync_DispatchEntityDomainEvents_DispatchAllAsync(
.HasManyForEachEntity(
x => x.Posts,
x => x.Blog,
- new EntityGenerator(new FakePost()).Setup(
- x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
+ new EntityGenerator(new FakePost()).Setup(x
+ => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var mediator = Substitute.For();
@@ -160,11 +165,10 @@ public async Task SaveEntitiesAsync_DispatchRelatedEntityDomainEvents_DispatchAl
.HasManyForEachEntity(
x => x.Posts,
x => x.Blog,
- new EntityGenerator(new FakePost()).Setup(
- x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
+ new EntityGenerator(new FakePost()).Setup(x
+ => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
db.Add(entity);
await db.SaveChangesAsync();
var mediator = Substitute.For();
@@ -195,11 +199,10 @@ public async Task SaveEntitiesAsync_DispatchEntityDomainEventsWithGeneratedId_Di
.HasManyForEachEntity(
x => x.Posts,
x => x.Blog,
- new EntityGenerator(new FakePost()).Setup(
- x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
+ new EntityGenerator(new FakePost()).Setup(x
+ => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
var mediator = Substitute.For();
var repository = new TestRepository(mediator, db);
@@ -226,11 +229,10 @@ public async Task SaveEntitiesAsync_DispatchEntityDomainEventsWithMultipleExcept
.HasManyForEachEntity(
x => x.Posts,
x => x.Blog,
- new EntityGenerator(new FakePost()).Setup(
- x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
+ new EntityGenerator(new FakePost()).Setup(x
+ => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)))
.GenerateSingle();
- var db = new FakeDbContext(
- new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options);
+ var db = GetFakeDbContext();
var mediator = Substitute.For();
mediator.Publish(Arg.Any(), Arg.Any())
.ThrowsAsync();
diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs
index 95f6495..13dfd37 100644
--- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs
+++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs
@@ -2,13 +2,8 @@
namespace Cnblogs.Architecture.UnitTests.Infrastructure.FakeObjects;
-public class FakeDbContext : DbContext
+public class FakeDbContext(DbContextOptions options) : DbContext(options)
{
- public FakeDbContext(DbContextOptions options)
- : base(options)
- {
- }
-
///
protected override void OnModelCreating(ModelBuilder modelBuilder)
{