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