惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

M
MIT News - Artificial intelligence
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
J
Java Code Geeks
G
Google Developers Blog
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
The Blog of Author Tim Ferriss
月光博客
月光博客
B
Blog
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
博客园_首页
人人都是产品经理
人人都是产品经理
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
Jina AI
Jina AI
S
SegmentFault 最新的问题
H
Help Net Security
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
Google DeepMind News
Google DeepMind News

博客园 - 东邪独孤

【ASP.NET Core】Cookie 验证中回调用 URL 自动跳转的问题 【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】通过 DbContext 选项扩展框架 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】优化后的模型 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能 【EF Core】带主键实体与无主键实体 【EF Core】框架是如何识别实体类的属性和主键的
【EF Core】FromExpression 方法有什么用?
东邪独孤 · 2025-10-14 · via 博客园 - 东邪独孤

比 90% 的人细心的大伙伴一定发现了 DbContext 类有一个方法叫 FromExpression,它到底干吗用的?官方文档中没有专门的介绍(只在表值函数映射的例子中看到)。

咱们先来看看此方法的签名:

IQueryable<TResult> FromExpression<TResult>(Expression<Func<IQueryable<TResult>>> expression)

看着好像很复杂的样子。其实不,咱们来拆解一下:

1、TResult 是类型参数(泛型的知识点没忘吧),这里其实指的就是实体类型,比如,你的爱狗 Dog。

2、这个方法返回 IQueryable<T> 类型,说明允许你使用 LINQ 查询。

3、重点理解其参数——Expression<TDelegate> 表达有个万能规律:可以把与 TDelegate 类型兼容的 lambda 表达式直接赋值给 Expression<> 变量。即这个 FromExpression 方法可以使用以下 lambda 表达式作为参数:

这个委托的意思就是:它,单身狗(无参数)一枚,但可以生产 IQueryable<T> 对象。

哦,说了一大堆,还没说这个方法到底有啥毛用。它的用处就是你可以指定一个表达式,让 EF 一开始就返回筛选过的查询。在 DbContext 的派生类中声明 DbSet<T> 类型的带 get + set 的公共属性这种生成首个查询(根查询)是最常用的方案,这个相信大伙们都很熟了,EF Core 最基础操作,老周就不多介绍了。假如你要对查询的初始数据做筛选,那么,按照 DbSet 的方案,要先执行一下 SELECT **** FROM #$$#*& 语句,然后再执行 SELECT **** FROM &*^$ WHERE xxxxx,我还没操作数据呢就执行了两次 SELECT 语句了。所以说,如果你一开始并不打算提取所有数据,那么直接从一开始就执行 SELECT **** FROM xxxx WHERE yyyy 多好,何必多浪费一条 SQL 语句?

还有一种使用场景:你的数据不是从某个表 SELECT 出来的,而是从一个表值函数返回的,这种情况也要借助 FromExpression 方法。

不知道老周以上说明你是否明白?不明白没关系,咱们实战一下你就懂了。

咱们先定义的实体:

/// <summary>
/// 妖书实体
/// </summary>
public class Book
{
    /// <summary>
    /// 标识 + 主键
    /// </summary>
    public Guid BookId { get; set; }
    /// <summary>
    /// 书名
    /// </summary>
    public string Title { get; set; } = string.Empty;
    /// <summary>
    /// 简介
    /// </summary>
    public string? Description { get; set; }
    /// <summary>
    /// 作者
    /// </summary>
    public string Author { get; set; } = string.Empty;
}

然后,继承 DbContext 类,常规操作。

public class MyDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.\\TEST;Database=mydb;Integrated Security=True;Trust Server Certificate=True");
                                // 配置日志
                                //.LogTo(log => Debug.WriteLine(log));
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>(t =>
        {
            t.ToTable("books", tb =>
            {
                tb.Property(x => x.BookId).HasColumnName("book_id");
                tb.Property(z=>z.Title).HasColumnName("title");
                tb.Property(k => k.Description).HasColumnName("desc");
                tb.Property(f => f.Author).HasColumnName("author");
            })
            .HasKey(t => t.BookId);
        });
    }

    // 以下行现在不需要了
    //public DbSet<Book> Books { get; set; }

    public IQueryable<Book> MyBooks
        => FromExpression(() => Set<Book>().Where(x => x.Author == "老周"));
}

这里不用再定义 DbSet<> 类型的属性了,因为我们要对数据进行筛选,重点看 MyBooks 属性的实现:

 public IQueryable<Book> MyBooks
     => FromExpression(() => Set<Book>().Where(x => x.Author == "老周"));

Set<Book>() 方法的调用会让 DbContext 自动在缓存字典中添加数据集合,然后一句 Where 做出筛选,上述代码的意思是只查询老周写的妖书,其他作者的不考虑。这时候 DbContext 不会发出 select * from xxx  SQL 语句,所以你不用担心执行多余的 SQL。调用 FromExpression 方法后,会使初始查询直接生成 Select * from xxx where ...... 语句,只查询一次。

现在往 SQL Server 中新建 mydb 数据库,并创建 books 表。

CREATE TABLE [dbo].[books] (
    [book_id] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
    [title]   NVARCHAR (35)    NOT NULL,
    [desc]    NVARCHAR (100)   NULL,
    [author]  NVARCHAR (20)    NOT NULL,
    PRIMARY KEY CLUSTERED ([book_id] ASC)
);

顺便向表中插入些测试数据。

insert into books (title, author, [desc])
values  (N'R语言从盗墓到考古',  N'张法师', N'一套不正经的R语言退隐教程'),
            (N'疯言疯语之HTML 6.0', N'老周', N'提前一个世纪出现的超文本协议'),
            (N'程序员风水学', N'孙电鹰', N'先匪后将的他,曾有“东陵大盗”之称,在盗掘过程中他学会了用风水理论去Debug项目'),
            (N'鸡形机器人编程入门', N'老周', N'未来,由于长期不种植作物,人类只能躺在病床上依靠吮吸预制营养液维持生命;后有人提出开发鸡形机器人,帮助人类进食')

现在,咱们试一下。

 using var context = new MyDbContext();
 foreach(Book bk in context.MyBooks)
 {
     Console.WriteLine($"{bk.Author,-10}{bk.Title}");
 }

如果日志启用,那么,你会看到,DbContext 从初始化到 foreach 循环访问数据,只生成了一条 SQL 语句。

SELECT [b].[book_id], [b].[author], [b].[desc], [b].[title]
    FROM [books] AS [b]
    WHERE [b].[author] = N'老周'

下面来看看另一种应用情形——映射表值函数。

先在 SQL Server 中创建一个内联表值函数,名为 get_all_books,返回表中所有行。

CREATE FUNCTION [dbo].[get_all_books]()
    RETURNS TABLE
        RETURN select * from dbo.books;

回到 .NET 项目,咱们要映射一下函数。

A、先在 DbContext 的派生类中定义一个方法,用于映射到函数,不需要实现方法体,直接抛异常就行。

internal IQueryable<Book> GetAllBooksMap()
{
    throw new NotSupportedException();
}

实际上,EF Core 并不会真正调用方法,只是通过生成表达式树 + 反射出方法名,然后再找到与方法名对应的数据库中的函数罢了。所以,方法不需要实现代码。

B、OnModelCreating 方法要改一下,映射列名的 HasColumnName 方法不能在 ToTable 方法中配置,否则表值函数返回的实体不能正确映射。

modelBuilder.Entity<Book>(t =>
{
    t.ToTable("books");
    t.HasKey(t => t.BookId);
    t.Property(x => x.BookId).HasColumnName("book_id");
    t.Property(z => z.Title).HasColumnName("title");
    t.Property(k => k.Description).HasColumnName("desc");
    t.Property(f => f.Author).HasColumnName("author");
});

也就是列名映射要在 Property 上配置,不能在 TableBuilder 上配置。

C、HasDbFunction 映射函数。

 // 注意数据库中的函数名与类方法不同
 modelBuilder.HasDbFunction(GetType().GetMethod(nameof(GetAllBooksMap), BindingFlags.NonPublic)!).HasName("get_all_books"); 

这里有个误区:很多大伙伴以为这样就完事了,然后就开始调用代码了。

using var context = new MyDbContext();
foreach(Book bk in context.GetAllBooksMap())
{
    Console.WriteLine($"{bk.Author,-10}{bk.Title}");
}

你以为这样是对的,但运行后就是错的。上面不是说了吗?GetAllBooksMap 方法是没有实现的,你不能直接调用它!不能调用,不能调用,不能调用!!

我们还需要再给 DbContext 的派生类再定义一个方法,使用 FromExpression 方法让 GetAllBooksMap 转为表达式树。

public IQueryable<Book> GetAllBooks()
{
   return  this.FromExpression(() => GetAllBooksMap());
}

这么一来,GetAllBooksMap() 就成了表达式树,EF 不会真的调用它,只是获取相关信息,再翻译成 SQL。

然后这样用:

using var context = new MyDbContext();
foreach(Book bk in context.GetAllBooks())
{
    Console.WriteLine($"{bk.Author,-10}{bk.Title}");
}

看,四条记录就读出来了。

image

可是,你也发现了,这TM太麻烦了,为了表值函数映射,我要封装两个方法成员。其实,这里可以把两个方法合成一个:

public IQueryable<Book> GetAllBooks()
{
   return  this.FromExpression(() => GetAllBooks());
}

由于是公共方法,OnModelCreating 中的 HasDbFunction 代码也可以精简一下。

modelBuilder.HasDbFunction(GetType().GetMethod(nameof(GetAllBooks))!).HasName("get_all_books"); 

这时候你又搞不懂了,What?GetAllBooks 方法怎么自己调用了自己?不不不,没有的事,你又忘了,FromExpression 只是转换为表达式树,并不会真的调用它。所以,这样合并后,其实代码是这样走的:

1、访问 context.GetAllBooks() ,这时候,GetAllBooks 方法确实被调用了,是你的代码调用的,不是EF调用;

2、GetAllBooks 方法被你调用后,FromExpression 方法被调用;

3、FromExpression 方法参数中,lambda 表达式虽然又引用了一次 GetAllBooks 方法,但这一次它不会被调用,EF Core 只是用来获取方法名。

现在明白了吗?

对,微软官方文档中的示例用的就是这种合并的方法,表面上看好像自己调用了自己,实则不会。

好了,今天就水到这里。