冻结 linq IQueryable(就像 ToList().AsQueryable() 会做的那样)

Freeze a linq IQueryable (as a ToList().AsQueryable() would do)

有没有办法冻结 IQueryable 以便在访问数据库时不会向查询添加额外的连接?例如,我可以执行 .ToList() 来冻结查询,但这会影响性能,因为我所做的任何过滤都是在中间层进行的,而我没有从数据库服务器上的预过滤中获得任何性能提升?


为清楚起见编辑:

我有一个 OData 服务,其中 returns 一个 IQueryable 客户端可以根据需要 filter/sort/project。我只是想阻止他们提取更多数据。我可以通过 ToList().AsQueryable() 来做到这一点,但这失去了 lazyLoading 的优势,并且失去了允许客户端过滤请求的全部目的。

我查看的一个选项是设置:EnableQueryAttribute.AllowedQueryOptions 以排除 Expand,但是即使我的初始查询已展开,客户端仍然无法选择这些部分。

所以我(认为)尝试了相同的方法,我发现的唯一解决方案是在 SQL 服务器管理器中使用 TVF。我假设 EF 6.1.1 和 Web API。以下是需要采取的一些步骤:

(1)创建一个Procedure(可以加参数):

CREATE PROCEDURE [dbo].[GetArticleApiKey] @a nvarchar(max) AS
SELECT  a.*
FROM    [dbo].[Blogs] b, [dbo].[Articles] a
WHERE   b.ApiKey = @a AND b.Id = a.Blog_Id

如果您在 Code-First 中需要这个,您可以在 DbContext 构造函数中使用带有 Database.SetInitializer 的 Initalizer。


(2)下载this nuget package. It enables Stored Procedures that are queryable. Here是项目库


(3) 将以下内容添加到您的 DbContext:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
     modelBuilder.Conventions.Add(new FunctionsConvention<YourContext>("dbo"));
}

(4) 将存储过程添加到您的上下文中:

public ObjectResult<Article> GetArticleApiKey(string apiKey)
{
 var apikeyParameter = new ObjectParameter(ApiKeyParameter, apiKey); // Make sure to validate this, because of sql injection
 ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<Article>("GetArticleApiKey", apikeyParameter);
}

(5) 您现在可以在控制器中使用此功能并进行预过滤。之后所有 Odata 查询仍然是可能的,但只能根据 sql-过程的结果。

context.GetArticleApiKey("MyApiKey").AsQueryable();

希望我答对了你的要求。

我假设您实际上有一个 IQueryable<T> 而不是 IQueryable

如果您不希望您的客户端访问所有 IQueryable<T> 方法,请不要 return IQueryable<T>。因为你希望他们只能 filter/sort/project,创建一个对象 包含一个 IQueryable<T> 但只公开所需的方法,并使用它:

public interface IDataResult<T>
{
    IDataResult<T> FilterBy(Expression<Func<T, bool>> predicate);

    IDataResult<TResult> ProjectTo<TResult>(Expression<Func<T, TResult>> predicate);

    IDataResult<T> SortBy<TKey>(Expression<Func<T, TKey>> keySelector);

    IDataResult<T> SortByDescending<TKey>(Expression<Func<T, TKey>> keySelector);

    List<T> ToList();

    IEnumerable<T> AsEnumerable();
}

public class DataResult<T> : IDataResult<T>
{
    private IQueryable<T> Query { get; set; }

    public DataResult(IQueryable<T> originalQuery)
    {
        this.Query = originalQuery;
    }

    public IDataResult<T> FilterBy(Expression<Func<T, bool>> predicate)
    {
        return new DataResult<T>(this.Query.Where(predicate));
    }

    public IDataResult<T> SortBy<TKey>(Expression<Func<T, TKey>> keySelector)
    {
        return new DataResult<T>(this.Query.OrderBy(keySelector));
    }

    public IDataResult<T> SortByDescending<TKey>(Expression<Func<T, TKey>> keySelector)
    {
        return new DataResult<T>(this.Query.OrderByDescending(keySelector));
    }

    public IDataResult<TResult> ProjectTo<TResult>(Expression<Func<T, TResult>> predicate)
    {
        return new DataResult<TResult>(this.Query.Select(predicate));
    }

    public List<T> ToList()
    {
        return this.Query.ToList();
    }

    public IEnumerable<T> AsEnumerable()
    {
        return this.Query.AsEnumerable();
    }
} 

这样您还可以防止 EF 和 DB 相关的依赖项在您的应用程序中蔓延。 IQueryable<T> 方法的任何更改都将包含在此 class 中,而不是遍布整个地方。

在我看来,最简单的方法就是将 IQueryable<T> 变成 Func<List<T>>。这样你就不会失去惰性方面,但你肯定会删除对数据库进行进一步连接的能力。

就是这么简单:

public static class FreezeEx
{

    public static Func<List<R>> Freeze<T, R>(this IQueryable<T> queryable, Expression<Func<T, R>> projection)
    {
        return () => queryable.Select(projection).ToList();
    }

    public static Func<List<R>> Freeze<T, R>(this IQueryable<T> queryable, Expression<Func<T, bool>> predicate, Expression<Func<T, R>> projection)
    {
        return () => queryable.Where(predicate).Select(projection).ToList();
    }
}

那么你可以这样做:

IQueryable<int> query = ...;

Func<List<int>> frozen = query.Freeze(t => t > 10, t => t);

List<int> results = frozen.Invoke();

下面是一些基本代码,以便对其进行测试:

public IEnumerable<int> Test()
{
    Console.WriteLine(1);
    yield return 1;
    Console.WriteLine(2);
    yield return 2;
}

现在我称之为:

IQueryable<int> query = Test().AsQueryable();

Console.WriteLine("Before Freeze");
Func<List<int>> frozen = query.Freeze(t => t > 10, t => t);
Console.WriteLine("After Freeze");

Console.WriteLine("Before Invokes");
List<int> results1 = frozen.Invoke();
List<int> results2 = frozen.Invoke();
Console.WriteLine("After Invokes");

在控制台上我得到这个:

Before Freeze
After Freeze
Before Invokes
1
2
1
2
After Invokes

你可以看到它是惰性的,它只在调用时运行。