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

作者:微信公众号:【架构师老卢】
10-17 19:0
149

作为 .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 请求参数,我们确保我们的应用程序保持高性能和用户友好性

相关留言评论
昵称:
邮箱:
阅读排行