Skip to content

Commit

Permalink
实现IQuery+IQueryHandler模式
Browse files Browse the repository at this point in the history
  • Loading branch information
witskeeper committed Nov 4, 2024
1 parent 87c7758 commit 93ee140
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 9 deletions.
17 changes: 17 additions & 0 deletions src/AspNetCore/IPagedQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NetCorePal.Extensions.Dto;
using NetCorePal.Extensions.Primitives;

namespace NetCorePal.Extensions.AspNetCore
{
public interface IPagedQuery<TResponse> : IQuery<PagedData<TResponse>>
{
public int PageIndex { get; }
public int PageSize { get; }
public bool CountTotal { get; }
}
}
29 changes: 22 additions & 7 deletions src/AspNetCore/PageQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ public static class PageQueryableExtensions
{
public static async Task<PagedData<T>> ToPagedDataAsync<T>(
this IQueryable<T> query,
int? index,
int? size,
bool? countTotal,
CancellationToken cancellationToken)
int pageIndex = 1,
int pageSize = 10,
bool countTotal = false,
CancellationToken cancellationToken = default)
{
var pageIndex = index ?? 1; // 默认取第1页
var pageSize = size ?? 10; // 默认每页10条
if (pageIndex <= 0)
{
throw new ArgumentOutOfRangeException(nameof(pageIndex), "页码必须大于 0");
}

if (pageSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(pageSize), "每页条数必须大于 0");
}

// isTotalNeeded为true时才查询总数。默认不需要总数
var totalCount = countTotal ?? false ? await query.CountAsync(cancellationToken) : 0;
var totalCount = countTotal ? await query.CountAsync(cancellationToken) : 0;

var items = await query.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
Expand All @@ -25,4 +32,12 @@ public static async Task<PagedData<T>> ToPagedDataAsync<T>(
return new PagedData<T>(items, totalCount, pageIndex, pageSize);
}


public static Task<PagedData<T>> ToPagedDataAsync<T>(
this IQueryable<T> query,
IPagedQuery<T> pagedQuery,
CancellationToken cancellationToken = default)
{
return query.ToPagedDataAsync(pagedQuery.PageIndex, pagedQuery.PageSize, pagedQuery.CountTotal, cancellationToken);
}
}
26 changes: 26 additions & 0 deletions src/AspNetCore/ServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Reflection;
using FluentValidation.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using NetCorePal.Extensions.AspNetCore;
using NetCorePal.Extensions.AspNetCore.Validation;
using NetCorePal.Extensions.Primitives;

namespace NetCorePal.Extensions.DependencyInjection;

Expand All @@ -25,4 +27,28 @@ public static MediatRServiceConfiguration AddKnownExceptionValidationBehavior(
cfg.AddOpenBehavior(typeof(KnownExceptionValidationBehavior<,>));
return cfg;
}



/// <summary>
/// 将所有实现IQuery接口的类注册为查询类,添加到容器中
/// </summary>
/// <param name="services"></param>
/// <param name="Assemblies"></param>
/// <returns></returns>

public static IServiceCollection AddAllQueries(this IServiceCollection services, params Assembly[] Assemblies)
{
foreach (var assembly in Assemblies)
{
//从assembly中获取所有实现IQuery接口的类
var queryTypes = assembly.GetTypes().Where(p => p.IsClass && !p.IsAbstract && p.GetInterfaces().Any(i => i == typeof(IQuery)));

Check warning on line 45 in src/AspNetCore/ServiceCollectionExtension.cs

View workflow job for this annotation

GitHub Actions / build

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)
foreach (var queryType in queryTypes)
{
//注册为自己
services.AddTransient(queryType, queryType);
}
}
return services;
}
}
23 changes: 23 additions & 0 deletions src/Primitives/IQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetCorePal.Extensions.Primitives
{


/// <summary>
/// 表示一个类为查询类,框架则会自动注册该类查询类
/// </summary>
public interface IQuery;


/// <summary>
/// 表示一个类为查询类,并指定查询结果类型,该Query由QueryHandler处理并相应
/// </summary>
/// <typeparam name="TResponse"></typeparam>
public interface IQuery<out TResponse> : IRequest<TResponse>;
}
16 changes: 16 additions & 0 deletions src/Primitives/IQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetCorePal.Extensions.Primitives;

public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
}



Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
Expand All @@ -23,6 +23,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AspNetCore\NetCorePal.Extensions.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\Context.AspNetCore\NetCorePal.Context.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\Context.Shared\NetCorePal.Context.Shared.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NetCorePal.Extensions.DependencyInjection;
using NetCorePal.Extensions.Primitives;

namespace NetCorePal.Context.AspNetCore.UnitTests
{

public class Query1 : IQuery
{
}


public class ServiceCollectionExtensionTests
{
[Fact]
public void AddAllQueriesTests()
{
var services = new ServiceCollection();
services.AddAllQueries(typeof(ServiceCollectionExtensionTests).Assembly);
Assert.Equal(1, services.Count);

Check warning on line 25 in test/NetCorePal.Context.AspNetCore.UnitTests/ServiceCollectionExtensionTests.cs

View workflow job for this annotation

GitHub Actions / build

Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
var provider = services.BuildServiceProvider();
var queryHandler = provider.GetRequiredService<Query1>();
Assert.NotNull(queryHandler);
}
}
}
23 changes: 22 additions & 1 deletion test/NetCorePal.Web.UnitTests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ public async Task PostTest()
}


[Fact]
public async Task QueryOrderByNameTest()
{
var client = factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/order", new CreateOrderRequest("na2", 55, 14), JsonOption);
Assert.True(response.IsSuccessStatusCode);
var r = await response.Content.ReadFromJsonAsync<OrderId>(JsonOption);
Assert.NotNull(r);


client = factory.CreateClient();
response = await client.GetAsync("/query/orderbyname?name=na2");
Assert.True(response.IsSuccessStatusCode);
var data = await response.Content.ReadFromJsonAsync<ResponseData<PagedData<GetOrderByNameDto>>>(JsonOption);
Assert.NotNull(data);
Assert.True(data.Success);
Assert.Single(data.Data.Items);
Assert.Equal("na2", data.Data.Items.First().Name);
}


[Fact]
public async Task SetPaidTest()
{
Expand Down Expand Up @@ -196,7 +217,7 @@ public async Task SetOrderItemNameTest()
Assert.False(queryResult.Paid);
Assert.Equal(1, queryResult.RowVersion.VersionNumber);
Assert.Single(queryResult.OrderItems);
Assert.Equal(1,queryResult.OrderItems.First().RowVersion.VersionNumber);
Assert.Equal(1, queryResult.OrderItems.First().RowVersion.VersionNumber);
}

[Fact]
Expand Down
22 changes: 22 additions & 0 deletions test/NetCorePal.Web/Application/Queries/GetOrderByNameQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using NetCorePal.Extensions.Dto;
using NetCorePal.Extensions.Primitives;

namespace NetCorePal.Web.Application.Queries
{


public record GetOrderByNameDto(OrderId OrderId, string Name);

public record GetOrderByNameQuery(string Name, int PageIndex = 1, int PageSize = 10, bool CountTotal = false) : IPagedQuery<GetOrderByNameDto>;


public class GetOrderByNameQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetOrderByNameQuery, PagedData<GetOrderByNameDto>>
{
public async Task<PagedData<GetOrderByNameDto>> Handle(GetOrderByNameQuery request, CancellationToken cancellationToken)
{
return await dbContext.Orders.Where(x => x.Name.Contains(request.Name))
.Select(p => new GetOrderByNameDto(p.Id, p.Name))
.ToPagedDataAsync(request, cancellationToken);
}
}
}
9 changes: 9 additions & 0 deletions test/NetCorePal.Web/Controllers/OrderController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ public Task<ResponseData<OrderId>> Path([FromRoute] OrderId id)
{
return Task.FromResult(new ResponseData<OrderId>(id));
}


[HttpGet]
[Route("/query/orderbyname")]
public async Task<ResponseData<PagedData<GetOrderByNameDto>>> QueryOrderByName([FromQuery] GetOrderByNameQuery query)
{
var data = await mediator.Send(query, HttpContext.RequestAborted);
return data.AsResponseData();
}
}

/// <summary>
Expand Down

0 comments on commit 93ee140

Please sign in to comment.