C# LINQ开发心得

LINQ

LINQ(Language Integrated Query,语言集成查询)是 .NET 框架中的一种查询技术,它允许开发者使用统一的语法 查询不同类型的数据源(如集合、数据库、XML 等,像List这样的集合类也是可以用LINQ的!)。

查询构建

  • 查询表达式 :使用类似 SQL 的语法(如 fromwhereselect
  • 方法语法 :使用 LINQ 扩展方法(如 Where()Select()OrderBy()
  • 两者可以混合使用,功能等价

这是查询表达式语法,很像SQL:

c# 复制代码
var query = from device in _context.Devices
            where device.Status == "online"
            orderby device.CreateTime descending
            select new DeviceDTO
            {
                Id = device.Id,
                Name = device.Name
                // 其他属性...
            };

var result = query.ToList();  // 执行查询

注意:LINQ SQL是直接写到C#代码里的!也就是说,它可以做编译期检查,而不像SQL语句,语法错误要到运行期才能发现(这在spring 里使用mybatis时是很常见的问题)。

这是LINQ方法语法:

c# 复制代码
// 构建查询(延迟执行)
var query = _context.Devices
    .Where(d => d.Status == "online")  // 过滤条件
    .OrderByDescending(d => d.CreateTime);  // 排序

// 执行查询(立即执行)
var onlineDevices = query.ToList();  // 此时才执行 SQL 查询

对于集合类操作而言,LINQ等价与java的stream API。

对于数据库而言,LINQ等价于java的Hibernate。

所以说,C#用一套LINQ技术,归一化了java里好几个领域的技术。

JOIN查询

再来个join语句:

linq 复制代码
from cp in _context.CollectPoints 
join device in _context.Devices on cp.DeviceId equals device.Id 
select new { CollectPoint = cp, DeviceName = device.Name };

该LINQ功能说明:

  1. 数据源定义

    复制代码
      from cp in _context.CollectPoints

    从CollectPoints表中获取所有记录,每个记录赋值给变量cp。

  2. 关联操作

    复制代码
      join device in _context.Devices on cp.DeviceId equals device.Id

    使用join关键字将CollectPoints表与Devices表关联,关联条件是cp.DeviceId等于device.Id

    这相当于 SQL 中的INNER JOIN操作,只返回两个表中匹配的记录。

  3. 结果投影

    复制代码
      select new { CollectPoint = cp, DeviceName = device.Name }

    使用select关键字定义查询结果的结构,创建一个匿名类型对象,包含两个属性:

    • CollectPoint:值为采集点对象 cp(包含采集点的所有字段)。
    • DeviceName:值为关联设备的 Name 属性(设备名称)。

技术原理:

  • LINQ 转换:这段代码会被 Entity Framework Core 转换为 SQL 语句,大致如下:

    sql 复制代码
    SELECT 
        cp.*, 
        d.Name AS DeviceName 
    FROM 
        CollectPoints cp
    INNER JOIN 
        Devices d ON cp.DeviceId = d.Id
  • 匿名类型new { ... } 创建的是一个编译时生成的匿名类型,用于临时存储查询结果,无需定义额外的实体类。

嵌套select

这是一个例子:

c# 复制代码
await query
            .OrderByDescending(p => p.CreateTime)
            .Skip((page - 1) * size)
            .Take(size)
            .Select(p => new AlarmPushDto
            {
                Id = p.Id,
                Name = p.Name,
                PushType = p.PushType,
                Enabled = p.Enabled,
                Remark = p.Remark,
                // 这里是嵌套select,注意,这里用的是同步ToList(),但整个LINQ还是ToListAsync(),这是没问题的
                Receivers = p.Receivers.Select(r => r.Receiver).ToList()
            })
            .ToListAsync();

使用上述嵌套select,要求在DBContext里配置HasMany导航属性:

c# 复制代码
modelBuilder.Entity<AlarmPush>()
             // 配置1:N的映射关系
            .HasMany(p => p.Receivers)
            .WithOne()
             // 配置关联字段,这里的ForeignKey并非物理数据库的外键,它只代表逻辑上的关联关系! 
            .HasForeignKey(r => r.PushId);

EF Core自动翻译的真实SQL为:

SQL 复制代码
SELECT `t0`.`id`, `t0`.`name`, `t0`.`push_type`, `t0`.`enabled`, `t0`.`remark`, `t1`.`receiver`, `t1`.`push_id`
      FROM (
          SELECT `t`.`id`, `t`.`name`, `t`.`push_type`, `t`.`enabled`, `t`.`remark`, `t`.`create_time`
          FROM `t_alarm_push` AS `t`
          ORDER BY `t`.`create_time` DESC
          LIMIT @__p_1 OFFSET @__p_0
      ) AS `t0`
      LEFT JOIN `t_alarm_push_receiver` AS `t1` ON `t0`.`id` = `t1`.`push_id`
      ORDER BY `t0`.`create_time` DESC, `t0`.`id`, `t1`.`push_id`

这里因为有分页,所以EF Core先做了一个含limit offset的子查询,然后用子查询结果跟子表做left join。

Include方法

配置了配置HasMany导航属性,也可用Include做表关联:

c# 复制代码
var entity = await context.AlarmPushes
                .Include(p => p.Receivers)
                .FirstOrDefaultAsync(p => p.Id == dto.Id);

EF Core翻译为:

sql 复制代码
SELECT `t0`.`id`, `t0`.`create_time`, `t0`.`enabled`, `t0`.`name`, `t0`.`push_type`, `t0`.`remark`, `t0`.`update_time`, `t1`.`push_id`, `t1`.`receiver`, `t1`.`create_time`, `t1`.`update_time`
      FROM (
          SELECT `t`.`id`, `t`.`create_time`, `t`.`enabled`, `t`.`name`, `t`.`push_type`, `t`.`remark`, `t`.`update_time`
          FROM `t_alarm_push` AS `t`
          WHERE `t`.`id` = @__dto_Id_0
          LIMIT 1
      ) AS `t0`
      LEFT JOIN `t_alarm_push_receiver` AS `t1` ON `t0`.`id` = `t1`.`push_id`
      ORDER BY `t0`.`id`, `t1`.`push_id`

也是子查询left join子表,跟嵌套select的翻译结果差不多。与嵌套select不同的是,Include会把子表的所有属性选出来,而嵌套select是可以指定部分属性的,更加灵活。

另外,两者用法上有点区别:Include常用于修改场景,因为它能获得实体的全属性,方便后续SaveChanges,所以一般不配AsNoTracking。而嵌套Select则常用于查询场景(因为可以精确到部分属性),一般会配置AsNoTracking。

多路径嵌套

当LINQ里只有一个Include或一个嵌套Select,我们称之为单路径嵌套,即A和B是1:N的关系。

但如果A和B是1:N,A和C是1:M,在LINQ里写了两个Include或两个嵌套Select,这就是多路径嵌套,很显然,最终SQL会是:

复制代码
A left join B left join C

这可能造成笛卡尔积爆炸,应尽量避免,可以将上述查询拆成两个查询执行,最后在内存里组装:

复制代码
ids = A left join B
select C where ids in ()

string的Contains方法

等价于like %%。

SQL注入

LINQ里只要不使用动态特性,天然就能防御SQL注入,比如:

c# 复制代码
var joinQry = from cp in context.CollectPoints
            join device in context.Devices on cp.DeviceId equals device.Id
            where device.Name == deviceName
            select cp.Code;

上述的where部分,框架会自动帮我们做参数化,不用考虑SQL注入问题。

所谓动态特性包括:FromSqlRaw、动态LINQ字符串等,动态特性就需要我们自己处理SQL注入问题了。

SelectMany

等价与java stream的flatMap

Cast和OfType

Cast:对集合里的每个元素做Cast转型

OfType:对集合里的每个元素做类型过滤。

相关推荐
雨落倾城夏未凉6 天前
第四章c#方法-参数数组和可选参数(16)
后端·c#
唐青枫7 天前
线程不是越多越快:C#.NET Thread 生命周期、同步与后台工作线程实战
c#·.net
唐青枫8 天前
别只会反射:C#.NET Emit 动态生成代码实战详解
c#·.net
咕白m6258 天前
.NET 环境下 Word 超链接批量提取方案
c#·.net
用户91721561902118 天前
C# 通信协议增量解析:用状态机处理半包和粘包
c#
小码编匠9 天前
C# 工控上位机必备:数据转换工具类与十个核心模块
后端·c#·.net
唐青枫11 天前
别再乱用 StartNew:C#.NET TaskFactory 任务调度实战详解
c#·.net
Artech11 天前
[MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力
ai·c#·agent·memory·maf
Scout-leaf13 天前
C#摸鱼实录——IoC与DI案例详解
c#
咕白m62513 天前
使用 C# 在 Excel 中应用多种字体样式
后端·c#