IQueryable和模型绑定魔术?

时间:2012-10-09 02:43:58

标签: c# asp.net

我正在测试Web Forms的新ASP.NET 4.5模型绑定,其中一个简单的存储库暴露了IQueryable。存储库使用的是EF 5,Database First方法。我正在预测EF自动生成的实体以使用我的DTO。

一切正常,这就是重点,我期待看到某种异常......

这是代码:

存储库

    public IQueryable<JobDto> GetJobs()
    {
        var ctx = this.contextResolver.GetCurrentContext<pubsEntities>();

        return ctx.jobs.Select(x => new JobDto
            {
                Description = x.job_desc,
                ID = x.job_id,
                Maximum = x.max_lvl,
                Minimum = x.min_lvl
            });
    }

正如您所看到的,我正在将我的EF实体投射到自定义DTO中,并且属性完全不同。

ASPX代码背后

    public IQueryable<JobDto> gv_GetData()
    {
        return this.jobsRepository.GetJobs();
    }

ASPX

    <asp:GridView runat="server" ID="gv" AllowPaging="true" AllowSorting="true"
        DataKeyNames="ID"
        AutoGenerateColumns="true"
        SelectMethod="gv_GetData"
        ItemType="QueryRepository.JobDto, QueryRepository">
        <Columns>
            <asp:BoundField DataField="Description" HeaderText="My custom description" SortExpression="Description" />
        </Columns>
    </asp:GridView>

当使用很好的存储库时,这就像魅力,分页和排序开箱即用(现在我不必使用ObjectDataSource来简化分页和分选)

我的问题是:

如果我的存储库返回IQueryable<JobDto>并且我的DTO的属性与我的EF实体的属性名称不同(这是一个名为job的不同实体)。

由于我的GridView配置了我的DTO实体中定义的属性名称,EF如何才能正确排序GridView?据我所知,使用LINQ进行动态排序是使用字符串来设置顺序标准。不知何故,LINQ to Entities - IQueryable将我的DTO属性自动映射到我的EF实体公开的属性。

任何人都可以帮助这个可怜的灵魂=(了解幕后发生的事情吗?

我运行SQL配置文件只是为了确认查询是否在数据库中正确执行:

SELECT TOP (10) 
[Project1].[C1] AS [C1], 
[Project1].[job_desc] AS [job_desc], 
[Project1].[job_id] AS [job_id], 
[Project1].[max_lvl] AS [max_lvl], 
[Project1].[min_lvl] AS [min_lvl]
FROM ( SELECT [Project1].[job_id] AS [job_id], [Project1].[job_desc] AS [job_desc], [Project1].[min_lvl] AS [min_lvl], [Project1].[max_lvl] AS [max_lvl], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[job_desc] DESC) AS [row_number]
    FROM ( SELECT 
        [Extent1].[job_id] AS [job_id], 
        [Extent1].[job_desc] AS [job_desc], 
        [Extent1].[min_lvl] AS [min_lvl], 
        [Extent1].[max_lvl] AS [max_lvl], 
        1 AS [C1]
        FROM [dbo].[jobs] AS [Extent1]
    )  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[job_desc] DESC

特别注意这些行(ASPX):

<asp:BoundField DataField="Description" SortExpression="Description" />

结果SQL

ORDER BY [Project1].[job_desc] DESC

3 个答案:

答案 0 :(得分:5)

使用此代码:

return ctx.jobs.Select(x => new JobDto
    {
        Description = x.job_desc,
        ID = x.job_id,
        Maximum = x.max_lvl,
        Minimum = x.min_lvl
    });

您正在调用的Select重载实际上是在创建一个新的IQueryable,它会影响EF发出的实际SQL。您还没有真正将数据对象投影到DTO,而是给EF一个表达式,它可以用来为结果集创建一个查询,该查询稍后会在查询运行后投射到您的DTO中。请注意,在被调用的Select重载中,正在传递Expression<Func<Job, JobDto>>,而不仅仅是Func<Job, JobDto>。因为EF分析表达式,所以它可以在可能的情况下对SQL进行复杂的转换。

OrderBy添加GridView表达式时,它只是使用可以转换为SQL的新表达式修改IQueryable

修改

当您在上下文的Select媒体资源上致电IQueryable<Job> jobs时,EF可以查看您的Expression<Job, JobDto>并确定:

  1. 您的JobDto投影
  2. 需要哪些列
  3. 如何从JobDto表格填充Job投影。
  4. 如果你看看BCL中的许多Expression类,你会看到它是如何解决这个问题的。当遇到x => new JobDto { Description = x.job_desc, ... }时,编译器会创建一个看起来像这样的复杂表达式树(我正在严格地简化这个):

    LambdaExpression<Func<Job, JobDto>>
        MemberInitExpression
            NewExpression
            Bindings
                MemberAssignment
                    Member = Description property
                    Expression = MemberExpression representing access to the Job property
                MemberAssignment...
                MemberAssignment...
                MemberAssignment...
                etc.
    

    您可以看到此树如何包含足够的信息,以便EF遍历表达式并生成内部映射并生成等效的SQL命令。他们基本上将.NET表达式投射到SQL表达式。并非所有内容都具有1:1映射,但在您的情况下,您可以看到映射是多么简单:

    Job type          -> Extent1 alias   -> dbo.jobs table
    JobDto projection -> Project1 alias  -> subquery
    

    还有其他预测,你会注意到;它引入了一个行号属性和一些包含值1的神秘属性;我不确定那是用来做什么的。

    OrderBy是一个额外的扩充,其中Expression被分析。

答案 1 :(得分:1)

我认为EF完全有可能知道如何生成查询,因为IQueryable可能在应用排序之前没有查询任何内容。我想如果你要运行它,你会得到相同的sql输出:

var ctx = this.contextResolver.GetCurrentContext<pubsEntities>();

var jobs = ctx.jobs.Select(x => new JobDto
    {
        Description = x.job_desc,
        ID = x.job_id,
        Maximum = x.max_lvl,
        Minimum = x.min_lvl
    }).OrderBy(x => x.Description).ToArray();

编辑:为后代保留我的答案,但雅各布的写得好得多。

答案 2 :(得分:1)

这个问题的答案在于IQueryable运算符的工作原理。由于LINQ查询是在延迟模式下执行的,因此只要调用GetJobs方法就不会获得结果。

要在ASP.NET GridView上启用排序,我们将属性SortExpression设置为列。当我们单击列来对数据进行排序时,会创建一个动态lambda表达式,并对从GridView的SelectMethod获得的结果调用OredrBy运算符。我们可以说,当我们应用排序时,会调用以下模式的声明:

gv_GetData().OrderBy(j => j.Description);

运算符的执行取决于GetJobs()方法返回的类型。再次降低另一个级别,我们可以说返回GridView的数据是:

的结果
this.jobsRepository.GetJobs().OrderBy(j => j.Description);

逐步降低其他级别,我们可以假设,将要评估的最终LINQ查询是:

ctx.jobs.Select(x => new JobDto
        {
            Description = x.job_desc,
            ID = x.job_id,
            Maximum = x.max_lvl,
            Minimum = x.min_lvl
        }).OrderBy(j => j.Description);

解析所有表达式后将形成SQL查询。这里,我们有2个表达式要解析:一个传递给Select运算符,另一个传递给OrderBy运算符。解析两个表达式后获得的数据用于形成SQL查询。

如果我们使用一个返回IEnumerable的方法作为SelectMethod,我们会得到一个异常。因为,IEnumerable运算符接受Func,它不能动态形成。