EF Core - GroupJoinメソッドで外部結合(leff outer join)する
EF CoreでGroupJoinメソッドを使って外部結合するサンプルです。
サンプルのテーブルとデータ
取得したいサンプルデータです。OuterとInnerを外部結合したいとします。
use Test;
drop table if exists dbo.[Outer];
drop table if exists dbo.[Inner];
create table dbo.[Outer](
	Id int not null,
	Value nvarchar(10) not null,
	constraint PK_Outer primary key(Id)
);
create table dbo.[Inner](
	Id int not null,
	Value nvarchar(10) not null,
	constraint PK_Inner primary key(Id)
);
insert into dbo.[Outer](Id, Value)
output inserted.*
values
	(1, N'a'),
	(2, N'b'),
	(3, N'c');
/*
Id	Value
1	a
2	b
3	c
*/
insert into dbo.[Inner](Id, Value)
output inserted.*
values
	(1, N'A'),
	(4, N'D');
/*
Id	Value
1	A
4	D
*/
-- GroupJoinを使って実行したいクエリ
select *
from dbo.[Outer]
	left outer join dbo.[Inner]
		on dbo.[Outer].Id = dbo.[Inner].Id
order by dbo.[Outer].Id;
/*
Id	Value	Id	Value
1	a	1	A
2	b	NULL	NULL
3	c	NULL	NULL
*/
GroupJoinメソッドを使って外部結合する
EF CoreでGroupJoinメソッドを使ってデータを取得してみます。
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using var context = new AppDbContext();
// GroupJoinを使って外部結合する
var samples = await context.Outers
	.GroupJoin(
		context.Inners,
		outer => outer.Id,
		inner => inner.Id,
		(outer, inners) => new {
			Outer = outer,
			Inners = inners,
		})
	.SelectMany(
		item => item.Inners.DefaultIfEmpty(),
		(item, inner) => new { Outer = item.Outer, Inner = inner })
	.ToListAsync();
// 実行されるSQL
/*
SELECT [o].[Id], [o].[Value], [i].[Id], [i].[Value]
FROM [Outer] AS [o]
LEFT JOIN [Inner] AS [i] ON [o].[Id] = [i].[Id]
*/
Console.WriteLine($"{nameof(Outer.Id)}\t{nameof(Outer.Value)}\t{nameof(Inner.Id)}\t{nameof(Inner.Value)}");
foreach (var entry in samples) {
	Console.WriteLine($"{entry.Outer.Id}\t{entry.Outer.Value}\t{entry.Inner?.Id}\t{entry.Inner?.Value}");
}
// 出力結果
/*
Id	Value	Id	Value
1	a	1	A
2	b
3	c
*/
public record Outer(int Id, string Value);
public record Inner(int Id, string Value);
public class AppDbContext : DbContext {
	public AppDbContext() {
		ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
	}
	public DbSet<Outer> Outers => Set<Outer>();
	public DbSet<Inner> Inners => Set<Inner>();
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
		var connectionString = new SqlConnectionStringBuilder {
			DataSource = ".",
			InitialCatalog = "Test",
			IntegratedSecurity = true,
		}.ToString();
		optionsBuilder.UseSqlServer(connectionString);
		optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
	}
	protected override void OnModelCreating(ModelBuilder modelBuilder) {
		modelBuilder.Entity<Outer>().ToTable(nameof(Outer));
		modelBuilder.Entity<Inner>().ToTable(nameof(Inner));
	}
}