在 .NET 中优化 API 性能:使用分页、筛选和投影实现高效的数据检索

科技   2024-10-27 06:18   上海  


作为 .NET 开发人员,有效管理大型数据集非常重要。获取不必要的数据会增加内存使用量并降低性能。为避免这种情况,我们可以创建处理筛选、分页、排序和将数据投影到特定格式的方法。这种方法可确保我们的应用程序使用更少的内存并更快地执行。

在本文中,我将向您展示如何在 .NET 中实现高效的查询系统。

介绍

在本文中,我将展示如何使用以下关键工具和技术在 .NET 中优化 API 性能:

  • LINQ Dynamic Core,用于根据用户输入进行动态排序和筛选。

  • Mapster 有效地将模型映射到 DTO,从而减少数据传输。

  • PredicateBuilder 创建灵活的动态过滤器。

  • IQueryable 替换为延迟执行,以便仅在必要时提取数据。

  • 用于一致地处理分页和排序的自定义属性

这些工具有助于确保高效的数据检索,减少内存使用并提高性能,即使对于大型数据集也是如此。

问题

获取大型数据集的所有数据可能会占用内存并降低系统速度。相反,我们应该只返回必要的数据并将其构建为 DTO(数据传输对象)。这确保我们只加载我们需要的内容并提高性能。

解决方案:GetProjectListAsync

此方法根据过滤器获取项目列表,应用分页,并对结果进行排序:

public async Task<PagedReadOnlyCollection<ProjectFilterDto>> GetProjectListAsync(
ProjectFilterDto filterDto,
PageableParams pagingParams,
SortParameter sortParameters)
{
var filter = CreateFilter(filterDto);

return await _projectRepository.GetPagedFilteredAndProjectedAsync<ProjectFilterDto>(
filter, pagingParams, sortParameters, nameof(DatasourceConstants.DesignCategory)
);
}

创建动态过滤器

该方法根据用户的输入构建一个过滤器,使查询更加灵活:CreateFilter

private static Expression<Func<Project, bool>> CreateFilter(ProjectFilterDto filterDto)
{
var predicate = PredicateBuilder.True<Project>();

if (!string.IsNullOrEmpty(filterDto.Title))
{
predicate = predicate.And(x => x.Title.Contains(filterDto.Title));
}

return predicate;
}

核心方法:GetPagedFilteredAndProjectedAsync

此方法处理筛选、分页、排序和投影到 DTO 的关键方面:GenericRepository<T>

public async Task<PagedReadOnlyCollection<TResult>> GetPagedFilteredAndProjectedAsync<TResult>(
Expression<Func<T, bool>> criteria,
PageableParams pagingParams,
SortParameter sortParameters,
params string[] includes)
where TResult : class
{
IQueryable<T> query = _context.Set<T>();

if (includes != null)
{
foreach (var include in includes)
{
query = query.Include(include);
}
}

query = query.Where(criteria);

var totalCount = await query.LongCountAsync();
if (totalCount == 0)
{
return new PagedReadOnlyCollection<TResult>(new List<TResult>(), totalCount);
}

if (!string.IsNullOrEmpty(sortParameters.SortBy))
{
query = query.OrderBy($"{sortParameters.SortBy} {sortParameters.SortDirection}");
}

var pagedOrdered = query.Skip((pagingParams.Page - 1) * pagingParams.Size)
.Take(pagingParams.Size);

var list = await pagedOrdered.ProjectToType<TResult>().ToListAsync();

return new PagedReadOnlyCollection<TResult>(list, totalCount);
}

动态表达式构建PredicateBuilder

这有助于创建灵活的数据筛选条件:PredicateBuilder

public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>()
{
return (T _) => true;
}

public static Expression<Func<T, bool>> False<T>()
{
return (T _) => false;
}

public static Expression<Func<T, bool>> Or<T>(
this Expression<Func<T, bool>> expression1,
Expression<Func<T, bool>> expression2)
{
InvocationExpression right = Expression.Invoke(expression2, expression1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expression1.Body, right), expression1.Parameters);
}

public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> expression1,
Expression<Func<T, bool>> expression2)
{
InvocationExpression right = Expression.Invoke(expression2, expression1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expression1.Body, right), expression1.Parameters);
}
}
  • True/False:这些方法返回所有元素的计算结果为 true 或 false 的谓词。

  • And/Or:这些方法允许动态组合多个条件。

支持类

两个帮助程序类用于管理分页和排序:

PageableParams

public class PageableParams
{
public int Size { get; init; }
public int Page { get; init; }
}

SortParameter

public class SortParameter
{
public string? SortBy { get; init; }
public string SortDirection { get; init; } = "asc";
}

处理 API 请求中的参数

为了解决这个问题,我创建了一个 API 方法,该方法允许根据用户提供的查询参数进行动态筛选、分页和排序。这可确保 API 仅返回必要的数据。

以下是 API 端点的实现:

[HttpGet("Filter")]
[PageableAndSortable]
[AllowAnonymous]
public async Task<IActionResult> GetProjects([FromQuery] ProjectFilterRequest model)
{
var projects = await _projectService.GetProjectListAsync(
model.Adapt<ProjectFilterDto>(),
_httpContextAccessor.GetPageableParams(),
_httpContextAccessor.GetSortParams<ProjctFilterResponse>()
);

return projects.TotalCount > 0
? new OkObjectResult(new { Data = projects.Adapt<IList<ProjctFilterResponse>>(), Count = projects.TotalCount })
: new NoContentResult();
}

ProjectFilterRequest 请求:

public sealed record ProjectFilterRequest
{
public string? Title { get; set; }
}

该类是包含用于筛选项目的属性的记录。在这种情况下,它包括一个可选属性,允许用户按标题搜索项目。ProjectFilterRequestTitle

ProjctFilterResponse

internal sealed record ProjctFilterResponse
{
[Required]
public string Id { get; init; } = default!;

[Required]
[SortableColumn(Name = "Title")]
public string Title { get; init; } = default!;

}

该类定义 API 返回的数据的结构。它包含:ProjctFilterResponse

  • Id:唯一标识项目的必需属性。

  • Title:表示项目标题且可排序的必需属性。

PageableAndSortableAttribute

[AttributeUsage(AttributeTargets.Method)]
public sealed class PageableAndSortableAttribute : Attribute
{
public PageableAndSortableAttribute(int defaultPageSize = Constants.Page.DefaultPageSize)
{
DefaultPageSize = defaultPageSize;
}

public int DefaultPageSize { get; }
}


这是可应用于 API 方法的自定义属性。它指定分页的默认页面大小。该属性允许开发人员为分页结果定义标准大小,从而确保整个 API 的一致性。PageableAndSortableAttribute

HttpContextAccessor扩展

public static class HttpContextAccessorExtensions
{
public static PageableParams GetPageableParams(this IHttpContextAccessor httpContextAccessor)
{
var hasPageNumber = int.TryParse(httpContextAccessor?.HttpContext?.Request.Query["Page"], out int pageNumber);
var hasPageSize = int.TryParse(httpContextAccessor?.HttpContext?.Request.Query["Size"], out int pageSize);

return new PageableParams
{
Page = !hasPageNumber || pageNumber <= 0 ? 1 : pageNumber,
Size = !hasPageSize || pageSize <= 0 ? Constants.Page.DefaultPageSize : pageSize,
};
}

public static SortParameter GetSortParams<T>(this IHttpContextAccessor httpContextAccessor)
where T : class
{
var sortBy = httpContextAccessor?.HttpContext?.Request.Query["SortBy"];
var sortDirection = httpContextAccessor?.HttpContext?.Request.Query["SortDirection"];

return new SortParameter
{
SortBy = sortBy,
SortDirection = (sortDirection == "ASC" || sortDirection == "DESC") ? sortDirection : "ASC"
};
}
}

该类为接口提供扩展方法。这些方法从 HTTP 请求查询中检索分页和排序参数:HttpContextAccessorExtensionsIHttpContextAccessor

  • GetPageableParams:提取分页参数( 和 )。PageSize

  • GetSortParams:提取排序参数 ( 和 )。SortBySortDirection

API 请求示例:

GET /api/projects/filter?Title=NewProject&Page=1&Size=10&SortBy=CreatedDate&SortDirection=ASC

此方法可确保仅获取必要的数据,从而保持较低的内存使用率并提高性能。通过使用 ,我们可以从延迟执行中受益,这意味着仅在需要时运行查询。此外,通过使用 ,我们可以只将必要的条件发送到查询,从而减少数据库的工作量。IQueryablePredicateBuilder

使用 of 至关重要,因为它允许我们仅以 DTO(数据传输对象)的形式返回必要的数据。我们不是返回整个模型及其所有字段,而是只检索手头操作所需的属性。这使我们的查询更加轻松,并确保我们不会因加载不必要的数据而浪费内存或带宽。ProjectToType

此外,通过自定义属性和扩展方法实现分页和排序,可实现简洁灵活的 API 设计。这种灵活性使用户能够根据特定需求自定义其请求,从而提高应用程序的整体响应能力和效率。通过有效管理 API 请求参数,我们确保我们的应用程序保持高性能和用户友好性

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章