【问题标题】:Freeze a linq IQueryable (as a ToList().AsQueryable() would do)冻结 linq IQueryable (就像 ToList().AsQueryable() 一样)
【发布时间】:2016-08-09 14:55:11
【问题描述】:

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


为清楚起见进行编辑:

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

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

【问题讨论】:

  • 不太清楚您想要“冻结”查询是什么意思。
  • 您是否有机会将谓词作为参数传入并将其用作过滤器,这样您就可以完全避免问题?
  • 如果在访问数据库时添加了其他连接,我怀疑这更多是您的 Linq 表达式的一个因素。请发布一些说明问题的代码。
  • 也许我是唯一的一个,但我可以看到 OP 的来源。他可能希望将IQueryable 从数据层传回,而不会产生ToList() 的成本,但要确保调用者不会弄乱查询。如果可能的话,我想知道。
  • 我不知道它是否有帮助,但尝试使用 AutoMapper 及其QueryableExtensions,您可以将返回的类型转换为新映射的IQueryable类型。将其想象为在 SQL 中创建视图,最终用户将很难选择更多数据,因为您没有设置映射,但他们仍然可以对其应用自己的过滤器。只需确保您的 DAL 实体设置为 internal,以便外部用户无法引用它们。如果可行,我会将此评论转为答案。

标签: c# linq entity-framework odata


【解决方案1】:

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

如果您不希望您的客户可以访问所有IQueryable<T> 方法,请不要返回IQueryable<T>。由于您希望他们只能过滤/排序/项目,因此创建一个包含 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&lt;T&gt; 方法的任何更改都将包含在此类中,而不是遍布各处。

【讨论】:

  • 我没有实际测试过这个,因为我不再从事这个项目了,但这似乎是最好的机制,所以我会接受答案。
【解决方案2】:

所以我(认为)尝试了同样的方法,我发现的唯一解决方案是在 SQL Server 管理器中使用 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 包。它启用可查询的存储过程。 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();

希望我猜对了你的要求。

【讨论】:

  • 是的,这是很多样板,将逻辑推送到数据库服务器,我更喜欢 AutoMapper 的 @ScottChamberlain 建议。但是,我真的只是希望有更简单的东西。
  • 是的。我对这个解决方案也不幸运。但是我搜索了很多并没有找到任何其他解决方案。自动映射器的想法很好,但对我来说不合适,因为我需要 linq to sql 而不是 linq to entity。如果您发现任何更好的,请告诉我!
【解决方案3】:

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

这就是这么简单:

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");

在控制台上我得到了这个:

冻结前 冻结后 调用前 1 2 1 2 调用后

您可以看到它是惰性的,并且仅在调用时运行。

【讨论】:

  • 是的,它是惰性的,但无论何时运行查询,它仍然是运行的完整范围。例如如果我只想要一条记录的一个字段,完整的查询仍会发送到数据库,而不是修剪 select 语句。
  • @JacobEggers - 你是说你希望能够过滤 (.Where) 但不加入吗?
  • @JacobEggers - 我做了一个更改,现在对你来说更灵活了。
  • 是的,但是当您冻结可查询对象时,查询会运行,这是我试图避免的开销。但是,我已经转移到其他项目,所以我现在不需要解决方案。
  • @JacobEggers - 不,当您冻结时查询不会运行。它仅在您调用 .Invoke() 时运行。
猜你喜欢
  • 2018-12-27
  • 1970-01-01
  • 1970-01-01
  • 2011-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多