はじめに:「LINQ式キャッシュ」は“同じクエリを何度も組み立てないための知恵”
LINQ を使っていると、こういうコードが増えがちです。
var q1 = users.Where(x => x.IsActive && x.Age >= 20);
var q2 = users.Where(x => x.IsActive && x.Age >= 20 && x.Age <= 30);
var q3 = users.Where(x => x.IsActive && x.Age >= 20 && x.Name.Contains("田"));
C#「毎回ほぼ同じ条件を書いている」「ちょっとずつ違うバリエーションが増えていく」
これが積み重なると、保守がつらくなります。
ここで出てくる考え方が LINQ式キャッシュ です。
ざっくり言うと、
「よく使う LINQ の“式(条件や並び替え)”を、再利用できる形でどこかに貯めておく」
という発想です。
ここでは、初心者向けに
LINQ の「式」と「結果」の違い
Expression<Func<T, bool>> を使った条件のキャッシュ
条件を組み合わせるためのユーティリティ
実務での「よく使う絞り込み・並び替え」を式としてキャッシュするイメージ
を、例題付きでかみ砕いて説明します。
「式」と「結果」を分けて考える
LINQ は「式」を組み立ててから「実行」している
まず、LINQ には大きく 2 種類あります。
配列や List に対する LINQ(LINQ to Objects)
DB(EF など)に対する LINQ(LINQ to Entities など)
どちらも、見た目は同じように書けますが、
内部的には「式(Expression)」として組み立ててから、
必要になったタイミングで実行されています。
例えば、こういうコード。
var query = users.Where(x => x.IsActive && x.Age >= 20);
// ここではまだ実行されていない(式だけ)
var list = query.ToList(); // ここで初めて実行される
C#ここでの重要ポイントは、「Where に渡している x => x.IsActive && x.Age >= 20 という“式”を、再利用できる形で持っておくと便利」ということです。
これが「LINQ式キャッシュ」の入り口です。
条件式を Expression としてキャッシュする
Func<T, bool> と Expression<Func<T, bool>> の違い
まず、よく見るこの形。
Func<User, bool> predicate = x => x.IsActive && x.Age >= 20;
C#これは「C# のコードとしての関数」です。
配列や List に対してはそのまま使えますが、
DB に対する LINQ(EF など)では「式ツリー」が必要になります。
using System.Linq.Expressions;
Expression<Func<User, bool>> expr = x => x.IsActive && x.Age >= 20;
C#Expression<Func<...>> は、「式そのもの」をオブジェクトとして持てる型です。
これをキャッシュしておくと、
何度も同じ Where 条件を使い回せる
別の条件と組み合わせて、新しい式を作れる
といったことができるようになります。
ここでの重要ポイントは、「“式をキャッシュしたい”ときは、Func ではなく Expression<Func<…>> を使う」ということです。
例1:よく使うフィルタ条件を式としてキャッシュする
「アクティブユーザー」の条件を 1 箇所にまとめる
User クラスを用意します。
public class User
{
public bool IsActive { get; set; }
public int Age { get; set; }
public string Name { get; set; } = "";
}
C#「アクティブユーザー」の条件を、あちこちでこう書いているとします。
var activeUsers = users.Where(x => x.IsActive);
var activeAndAdult = users.Where(x => x.IsActive && x.Age >= 20);
C#これを「式としてキャッシュ」してみます。
using System.Linq.Expressions;
public static class UserPredicates
{
public static readonly Expression<Func<User, bool>> IsActive
= x => x.IsActive;
public static readonly Expression<Func<User, bool>> IsAdult
= x => x.Age >= 20;
}
C#使う側はこう書けます。
var activeUsers = users.Where(UserPredicates.IsActive);
var activeAndAdult = users.Where(UserPredicates.IsActive)
.Where(UserPredicates.IsAdult);
C#DB に対する LINQ(EF など)でも、そのまま使えます。
ここでの重要ポイントは、「“よく使う条件”を Expression として static に持っておくと、コピペを減らせて、変更も 1 箇所で済む」ということです。
条件式を組み合わせるユーティリティ
Expression を AND / OR でつなげたい
「アクティブかつ成人」のように、式同士を組み合わせたい場面が出てきます。
単純に UserPredicates.IsActive && UserPredicates.IsAdult とは書けません。
そこで、Expression を合成するユーティリティを用意します。
細かい仕組みは一旦置いて、使い方のイメージから見てください。
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var param = Expression.Parameter(typeof(T), "x");
var body = Expression.AndAlso(
Expression.Invoke(left, param),
Expression.Invoke(right, param));
return Expression.Lambda<Func<T, bool>>(body, param);
}
public static Expression<Func<T, bool>> Or<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var param = Expression.Parameter(typeof(T), "x");
var body = Expression.OrElse(
Expression.Invoke(left, param),
Expression.Invoke(right, param));
return Expression.Lambda<Func<T, bool>>(body, param);
}
}
C#これを使うと、こう書けます。
var activeAndAdultExpr =
UserPredicates.IsActive.And(UserPredicates.IsAdult);
var result = users.Where(activeAndAdultExpr);
C#ここでの重要ポイントは、「Expression を“部品”として持っておき、AND / OR で組み合わせて新しい式を作れる」ということです。
これが「LINQ式キャッシュ」の一番おいしいところです。
例2:検索画面の条件を動的に組み立てる
「入力された項目だけ条件にする」典型パターン
検索画面で、こういう項目があるとします。
アクティブのみ(bool? isActiveFilter)
最小年齢(int? minAge)
名前に含まれる文字列(string? nameContains)
入力されたものだけ条件にしたい。
これを Expression で組み立ててみます。
Expression<Func<User, bool>> BuildUserFilter(
bool? isActiveFilter,
int? minAge,
string? nameContains)
{
Expression<Func<User, bool>> predicate = x => true; // 何もしない初期値
if (isActiveFilter == true)
{
predicate = predicate.And(UserPredicates.IsActive);
}
if (minAge.HasValue)
{
Expression<Func<User, bool>> ageExpr = x => x.Age >= minAge.Value;
predicate = predicate.And(ageExpr);
}
if (!string.IsNullOrEmpty(nameContains))
{
Expression<Func<User, bool>> nameExpr = x => x.Name.Contains(nameContains);
predicate = predicate.And(nameExpr);
}
return predicate;
}
C#使う側はこうです。
var filter = BuildUserFilter(true, 20, "田");
var result = users.Where(filter).ToList();
C#ここでの重要ポイントは、「“検索条件を Expression として組み立ててキャッシュしておくと、DB に対する LINQ でもそのまま使える”」ということです。
条件の組み立てロジックと、実際のクエリ実行をきれいに分離できます。
「式そのもの」をキャッシュする意味
同じクエリ構造を何度も使うときに効いてくる
LINQ to Entities(EF など)では、
「式ツリーを解析して SQL に変換する」コストがそれなりにあります。
同じような Where / Select / OrderBy を何度も書いていると、
毎回「式の構造を解析→SQL 生成」が走ります。
式を 1 回組み立てて static に持っておき、
パラメータだけ変えて使い回すことで、
クエリ構造の再利用
条件の一元管理
バグの混入ポイントの削減
といったメリットが出てきます。
例えば、「ユーザー一覧の標準クエリ」を式として持っておくイメージです。
public static class UserQueries
{
public static readonly Expression<Func<IQueryable<User>, IQueryable<User>>> ActiveOrdered
= q => q.Where(x => x.IsActive)
.OrderBy(x => x.Name);
}
C#使う側はこうです。
var query = UserQueries.ActiveOrdered.Compile()(db.Users);
var list = query.ToList();
C#ここでの重要ポイントは、「“クエリの形”を式としてキャッシュしておくと、“どの画面も同じロジックでデータを取っている”ことを保証しやすい」ということです。
バラバラに LINQ を書くより、設計として強くなります。
実務での「LINQ式キャッシュ」の使いどころ
よく使うフィルタ・並び順・ページング
例えば、ユーザー一覧の標準クエリを考えます。
アクティブのみ
名前昇順
ページング(ページ番号と件数)
これを毎回バラバラに書くのではなく、「式+メソッド」としてまとめておきます。
public static class UserQueryExtensions
{
public static IQueryable<User> ApplyDefaultFilter(this IQueryable<User> q)
=> q.Where(x => x.IsActive);
public static IOrderedQueryable<User> ApplyDefaultOrder(this IQueryable<User> q)
=> q.OrderBy(x => x.Name);
public static IQueryable<User> ApplyPaging(this IQueryable<User> q, int page, int pageSize)
=> q.Skip((page - 1) * pageSize).Take(pageSize);
}
C#使う側はこうです。
var query = db.Users
.ApplyDefaultFilter()
.ApplyDefaultOrder()
.ApplyPaging(page, pageSize);
var list = query.ToList();
C#ここでは Expression を直接触っていませんが、
「クエリの形を再利用する」という意味で、
“LINQ式キャッシュ”と同じ発想です。
ここでの重要ポイントは、「“よく使うクエリの形”を、拡張メソッドや Expression としてどこかにまとめておくと、業務コードが一気に読みやすくなる」ということです。
まとめ:「LINQ式キャッシュ」は“LINQ をコピペしないための設計テクニック”
LINQ式キャッシュの本質は、
Where や OrderBy の「式そのもの」を
Expression や拡張メソッドとして再利用可能な形にしておき、
同じクエリ構造を何度も書かないようにする
ことです。
押さえておきたいポイントを整理すると、
「式そのもの」を扱いたいときは Func ではなく Expression<Func<…>> を使う
よく使う条件は static な Expression としてキャッシュしておくと、コピペが減る
Expression を AND / OR で合成するユーティリティ(PredicateBuilder 的なもの)があると、動的検索が書きやすい
「クエリの形」を拡張メソッドや Expression としてまとめておくと、どの画面も同じロジックでデータを取れる
ここまで腹落ちしていれば、
「その場その場で LINQ を書き散らす」段階から抜けて、
“LINQ の式を設計して再利用する”という一段上の書き方に踏み出せます。

