GitHubの公式ドキュメント更新「Breaking change doc: Nullable.GetUnderlyingType throws for custom Type subclasses (.NET 11)」でまず確認すべきことは、.NET 11 Preview 4以降、カスタム System.Type サブクラスを Nullable.GetUnderlyingType(Type) に渡すと、従来のように null が返るのではなく NotSupportedException が発生する可能性があるという点です。通常の typeof(int?) のようなランタイム型を扱うコードは大きな影響を受けにくい一方、リフレクション基盤、メタデータ解析、コード生成、独自型モデルを持つライブラリでは移行準備が必要です。(GitHub)
今回の更新は、単なるドキュメント追加ではありません。.NET 11への移行時に「ビルドは通るが、実行時に例外が出る」タイプの不具合を防ぐための確認ポイントです。特に、System.Type を継承した独自クラス、MetadataLoadContext を使ったアセンブリ解析、nullable判定を行う共通ライブラリを持つチームは、早めに対象コードを洗い出しておきましょう。
今回のGitHub公式ドキュメント更新で何が変わったか
今回のMicrosoftDocs系のGitHub更新では、.NET 11の破壊的変更として「Nullable.GetUnderlyingType がカスタム Type サブクラスに対して例外を投げる場合がある」ことが明文化されました。Microsoft Learn上の該当ページでは、この変更は .NET 11 Preview 4 で導入されたものとして説明されています。(Microsoft Learn)
| 確認項目 | 内容 |
|---|---|
| 対象API | System.Nullable.GetUnderlyingType(Type) |
| 新しく関係するAPI | System.Type.GetNullableUnderlyingType() |
| 変更種別 | Behavioral change、つまり実行時の動作変更 |
| 影響を受けやすいコード | カスタム Type サブクラスを定義しているコード |
| 主な症状 | 従来 null だった戻り値が、NotSupportedException になる可能性 |
| すぐ行うべきこと | Type 継承クラスと Nullable.GetUnderlyingType の利用箇所を検索する |
.NET 11の破壊的変更一覧でも、この項目はCore .NET librariesの「Behavioral change」として扱われています。Behavioral changeは、既存コードのコンパイル自体は通っても、実行時の結果や例外発生が変わる可能性がある変更です。(Microsoft Learn)
従来の動作と.NET 11での新しい動作
従来の Nullable.GetUnderlyingType(Type) は、渡された型が Nullable<T> であれば T を返し、そうでなければ null を返す、という使い方が一般的でした。
例えば、通常のリフレクションであれば次のように使われます。
Type type = typeof(int?);
Type? underlyingType = Nullable.GetUnderlyingType(type);
Console.WriteLine(underlyingType); // System.Int32
一方、int のように Nullable<T> ではない型を渡すと、通常は null が返ります。
Type type = typeof(int);
Type? underlyingType = Nullable.GetUnderlyingType(type);
Console.WriteLine(underlyingType == null); // True
問題になるのは、System.Type を継承した独自クラスを使っている場合です。.NET 11では、Nullable.GetUnderlyingType(Type) が新しい仮想メソッド Type.GetNullableUnderlyingType() に処理を委譲します。そのため、この新しいメソッドをオーバーライドしていないカスタム Type サブクラスでは、ベース実装により NotSupportedException が発生します。(Microsoft Learn)
class MyType : Type
{
// GetNullableUnderlyingType をオーバーライドしていない
}
Type type = new MyType();
Type? underlyingType = Nullable.GetUnderlyingType(type);
// .NET 11では NotSupportedException が発生する可能性
ここで重要なのは、すべての Nullable.GetUnderlyingType 利用箇所が危険というわけではないことです。危険なのは、通常の RuntimeType ではなく、独自実装の Type オブジェクトを渡しているケースです。
影響を受けやすいコード、受けにくいコード
実務では、まず「自分たちのコードがこの変更に該当するか」を判断する必要があります。次の表を目安に確認してください。
| 利用シーン | 影響度 | 確認ポイント |
|---|---|---|
typeof(int?) や PropertyInfo.PropertyType を通常の実行時型で扱う | 低 | 多くの場合、従来どおり利用できる |
class XxxType : Type のように System.Type を直接継承している | 高 | GetNullableUnderlyingType() の実装が必要 |
| 独自のリフレクションモデル、型ラッパー、型プロキシを持つ | 高 | 内部の型判定ロジックを見直す |
MetadataLoadContext を使ってアセンブリを解析している | 中〜高 | nullable判定やNullabilityInfoの結果を再検証する |
| ソースジェネレーター、ORM、シリアライザー、DIコンテナで型解析を行う | 中 | カスタム型モデルを使っていないか確認する |
| GitHub Actionsで.NET 11 Preview SDKを使ったテストをしている | 中 | CIで実行時例外として検出される可能性がある |
Microsoft Learnでは、.NETに同梱されている主要な Type サブクラスは新しい仮想メソッドをオーバーライドしており、影響を受けないと説明されています。対象には、ランタイムの Type 実装、TypeDelegator、TypeBuilder、EnumBuilder、MetadataLoadContext 関連の型などが含まれます。(Microsoft Learn)
そのため、一般的なアプリ開発者よりも、ライブラリ作者、フレームワーク開発者、解析ツール開発者、アーキテクトが優先して確認すべき変更です。
なぜこの変更が入ったのか
この変更の背景には、従来の Nullable.GetUnderlyingType(Type) が「現在実行中のランタイムが持つ Nullable<>」を前提に判定していた問題があります。
GitHub上のruntime issueでは、MetadataLoadContext から取得した Type に対して Nullable.GetUnderlyingType() を呼ぶと、実際には Nullable<T> であっても null が返る問題が報告されています。原因として、typeof(Nullable<>) との参照比較が、RuntimeType と MetadataLoadContext 側の型表現の違いによって失敗することが説明されています。(GitHub)
つまり、今回の変更は「例外が増えた」というだけではありません。別のリフレクション世界に属する Nullable<T> を正しく扱えるようにするため、Type 実装側に nullable判定の責任を持たせる設計変更です。
API提案でも、Nullable.GetUnderlyingType(Type) が新しい Type.GetNullableUnderlyingType() に処理を委譲する方針が示され、MetadataLoadContext の型でも正しくnullableの基底型を取得できることが狙いとして説明されています。([GitHub][5])
まず確認すべきコード検索ポイント
.NET 11対応の初動では、広く検索してから影響度を絞り込むのが効率的です。特に大規模なリポジトリでは、いきなり修正するよりも、検索結果を「危険」「要確認」「影響なし」に分類しましょう。
System.Type を継承しているクラスを探す
まず、独自の Type サブクラスがあるかを確認します。
: Type
: System.Type
extends Type
C#であれば、次のような定義がないかを見ます。
public class CustomType : Type
{
}
このようなクラスが見つかった場合、.NET 11向けに GetNullableUnderlyingType() のオーバーライドを検討する必要があります。
Nullable.GetUnderlyingType の呼び出し箇所を探す
次に、nullable判定を行っている箇所を確認します。
Nullable.GetUnderlyingType
GetUnderlyingType(
特に注意したいのは、呼び出し元で渡している Type がどこから来ているかです。
var type = propertyInfo.PropertyType;
var underlying = Nullable.GetUnderlyingType(type);
このように通常のリフレクションから得た型だけを扱っている場合は、影響が小さい可能性があります。
一方で、次のようなコードでは注意が必要です。
Type type = customTypeProvider.ResolveType(name);
Type? underlying = Nullable.GetUnderlyingType(type);
customTypeProvider が独自の Type サブクラスを返しているなら、.NET 11で例外に変わる可能性があります。
カスタムTypeサブクラス側で必要な対応
カスタム Type サブクラスを作っている場合は、GetNullableUnderlyingType() をオーバーライドします。対応方針は、そのクラスが Nullable<T> を表現し得るかどうかで変わります。
Nullableを表現しないTypeならnullを返す
独自の Type クラスが Nullable<T> を表現しない設計なら、明示的に null を返します。
public override Type? GetNullableUnderlyingType()
{
return null;
}
この対応は単純ですが、重要です。何もしないままだと、Nullable.GetUnderlyingType(customType) を呼んだ側で NotSupportedException が発生する可能性があります。
Nullableを表現する可能性があるなら基底型を返す
独自型がジェネリック型を表現し、Nullable<T> になる可能性がある場合は、T に相当する型を返します。
public override Type? GetNullableUnderlyingType()
{
if (IsGenericType
&& !IsGenericTypeDefinition
&& GetGenericTypeDefinition() == typeof(Nullable<>))
{
return GetGenericArguments()[0];
}
return null;
}
ただし、独自のリフレクション世界を持つライブラリでは、GetGenericTypeDefinition() == typeof(Nullable<>) だけでは不十分な場合があります。MetadataLoadContext の問題と同じく、型の表現がランタイムの typeof(Nullable<>) と一致しない可能性があるためです。
その場合は、以下の観点で自分たちの型モデルに合わせた判定を実装します。
| 確認観点 | 例 |
|---|---|
| 型名 | System.Nullable<T> を表す型か |
| 名前空間 | System か |
| ジェネリック引数 | 1つだけか |
| 型定義の同一性 | 自分たちの型モデル上で Nullable<> と同じか |
| 戻り値 | Nullable<T> の T に相当する Type を返せるか |
「名前が同じならNullableとみなす」という実装は手軽ですが、ユーザー定義型と誤判定する余地があります。API提案でも、名前ベースのフォールバックはユーザー定義型と一致するリスクがあるため代替案として退けられています。([GitHub][5])
別のTypeを包むラッパーなら内部Typeに委譲する
独自 Type が別の Type をラップしているだけなら、内部の型に処理を委譲するのが自然です。
private readonly Type _innerType;
public override Type? GetNullableUnderlyingType()
{
return _innerType.GetNullableUnderlyingType();
}
この実装では、ラッパー自体がnullable判定のロジックを持たず、内側の Type が持つ判定結果をそのまま利用します。
呼び出し側で例外を握りつぶすべきか
移行時にやりがちな対応として、Nullable.GetUnderlyingType を try-catch で囲む方法があります。
Type? underlyingType;
try
{
underlyingType = Nullable.GetUnderlyingType(type);
}
catch (NotSupportedException)
{
underlyingType = null;
}
これは一時回避としては使えますが、根本対応としてはおすすめしにくい方法です。理由は、NotSupportedException が「この型実装はnullable判定に答えられない」というシグナルだからです。
特にライブラリやフレームワークでは、例外を握りつぶして null 扱いにすると、次のような不具合につながります。
| 握りつぶしのリスク | 起こり得る問題 |
|---|---|
Nullable<T> を非nullableとして扱う | バリデーションやスキーマ生成が誤る |
| NullabilityInfoの結果がずれる | API契約やDTO生成が不正確になる |
| メタデータ解析結果が変わる | コード生成やマッピングが壊れる |
| 例外原因が見えなくなる | .NET 11移行時の不具合調査が難しくなる |
アプリケーション側で一時的に影響を抑えるなら、ログを残したうえで回避する方が安全です。
Type? TryGetNullableUnderlyingType(Type type, ILogger logger)
{
try
{
return Nullable.GetUnderlyingType(type);
}
catch (NotSupportedException ex)
{
logger.LogWarning(ex, "Type does not support nullable underlying type resolution: {Type}", type);
return null;
}
}
ただし、カスタム Type の作者であれば、呼び出し側で回避するよりも GetNullableUnderlyingType() を正しく実装することを優先してください。Microsoft Learnでも、カスタム Type サブクラスの作者にはこのメソッドのオーバーライドが推奨されています。(Microsoft Learn)
移行準備で見るべき実務チェックリスト
.NET 11対応の作業では、次の順で進めると抜け漏れを減らせます。
| 手順 | 作業 | 判断基準 |
| -: | —————————————– | ———————————- |
| 1 | Type 継承クラスを検索する | 自社コード、共通ライブラリ、社内NuGetを含める |
| 2 | Nullable.GetUnderlyingType の呼び出し箇所を検索する | 引数の Type がどこ由来か確認する |
| 3 | カスタム Type が Nullable<T> を表現するか分類する | 表現しないなら null、表現するなら基底型を返す |
| 4 | .NET 11 Preview SDKでテストする | コンパイルだけでなく実行テストを見る |
| 5 | 例外ログを確認する | NotSupportedException の発生箇所を特定する |
| 6 | ライブラリ利用者向けの互換性メモを用意する | public APIで Type を受け取る場合は特に重要 |
| 7 | CI/CDに.NET 11検証ジョブを追加する | GitHub Actionsなどで早期検知する |
ここでのポイントは、「Nullable.GetUnderlyingType を使っているか」だけで判断しないことです。通常のランタイム型だけを扱うコードと、独自 Type を扱うコードでは、リスクが大きく違います。
GitHub Actionsやクラウド運用での注意点
今回の更新はGitHub上の公式ドキュメント更新として確認できますが、GitHubの設定変更やGitHub Actions自体の仕様変更ではありません。影響の中心は、.NET 11を使ってアプリやライブラリをビルド・実行する開発環境です。
ただし、運用面ではGitHub ActionsなどのCI環境に影響が出ることがあります。
例えば、次のようなワークフローでは、.NET 11 Previewの導入後にテストが失敗する可能性があります。
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '11.0.x'
- name: Test
run: dotnet test
特に、テスト対象に以下が含まれる場合は注意してください。
- 型解析を行うライブラリ
- リフレクションを多用する共通基盤
- コード生成ツール
- 独自メタデータローダー
MetadataLoadContextを使う検査ツール- nullable情報をもとにスキーマやDTOを生成する処理
クラウド管理者やテクニカル意思決定者にとっては、「.NET 11に上げるかどうか」だけでなく、どのCIジョブでPreview SDKを先行検証するかが重要です。本番アプリの移行前に、ライブラリ・ツール・コード生成系のテストを先に回すと、影響範囲を早く把握できます。
よくある誤解
Nullable reference typesの変更ではない
今回の変更は、C# 8以降のnullable reference types、つまり string? のような参照型の注釈そのものを直接変更するものではありません。
対象は主に Nullable<T>、つまり次のような値型のnullableです。
int?
DateTime?
bool?
ただし、NullabilityInfoContext やメタデータ解析では、値型nullableの判定結果が参照型nullableの解析結果にも影響する場面があります。GitHubのruntime issueでも、MetadataLoadContext 由来の型で NullabilityInfo の結果が不正確になる例が示されています。(GitHub)
GitHubの利用者全員に影響するわけではない
「GitHubの公式ドキュメント更新」と聞くと、GitHub利用者全員に影響するように見えますが、実際には.NET 11のAPI動作変更に関する情報です。
GitHub上のリポジトリでコードを管理しているだけなら、直接の影響はありません。影響が出るのは、.NET 11環境で対象コードをビルド・実行する場合です。
null が返らないこと自体がバグではない
従来 null が返っていた場面で例外が出ると、単純に「互換性が壊れた」と見えます。しかし、今回の変更では、カスタム Type がnullable判定に正しく答えるためのフックを提供することが目的です。
つまり、NotSupportedException は「この型はnullableではない」という意味ではありません。この型実装は、nullableの基底型を返す処理をまだ提供していないという意味で捉えるべきです。
チームで共有すべき判断基準
開発チーム内では、次のように役割ごとに確認ポイントを分けると対応が進めやすくなります。
| 役割 | 確認すべきこと |
|---|---|
| アプリ開発者 | Nullable.GetUnderlyingType の利用箇所で、独自 Type が渡っていないか |
| ライブラリ作者 | System.Type 継承クラスに GetNullableUnderlyingType() を実装しているか |
| アーキテクト | 型解析基盤、コード生成、メタデータ解析への影響範囲 |
| クラウド管理者 | CIで.NET 11 Preview SDKを使うジョブの失敗状況 |
| 技術意思決定者 | .NET 11移行計画に互換性検証の期間を含めるか |
特に社内共通ライブラリで Type を抽象化している場合、そのライブラリを直さない限り、複数プロジェクトで同じ例外が発生する可能性があります。個別アプリ側で対処するより、共通基盤側で GetNullableUnderlyingType() を実装する方が保守しやすいでしょう。
まとめ:まずはType継承とNullable判定を棚卸しする
今回のGitHub公式ドキュメント更新で押さえるべき結論は明確です。.NET 11では、Nullable.GetUnderlyingType(Type) が新しい Type.GetNullableUnderlyingType() に処理を委譲するため、カスタム Type サブクラスを使っているコードでは NotSupportedException が発生する可能性があります。
まず行うべきことは、次の3つです。
System.Typeを継承している独自クラスを検索するNullable.GetUnderlyingTypeの呼び出し箇所を洗い出す- カスタム
TypeにGetNullableUnderlyingType()の実装が必要か判断する
通常のアプリコードでは影響が限定的な場合もありますが、リフレクション、メタデータ解析、コード生成、ライブラリ開発に関わるチームは早めの確認が必要です。.NET 11移行を予定しているなら、Preview SDKでの実行テストを追加し、null 前提のnullable判定が例外に変わらないかをチェックしておきましょう。

コメント