複数プロジェクトで .NET Framework 4.8 から .NET 8(net8.0)へ段階移行していると、参照エラーや Upgrade Assistant の候補不表示など、移行作業が止まりがちです。本記事では、net8.0 を参照できない根本原因と、Controller がドロップダウンに出ない時の実務的な直し方を具体例付きで整理します。
.NET Framework 4.8 のプロジェクトから net8.0 を参照できないエラーの正体
最初に結論から言うと、.NET Framework 4.8(net48)は net8.0 をターゲットにしたライブラリを参照できません。これは「設定ミス」ではなく、ターゲット フレームワーク(TFM: Target Framework Moniker)の互換性ルールによるものです。
そのため、Folder / Folder1 / Folder2 を net8.0 に移行して先に進めたとしても、それらを参照する Folder3 が(見た目は変えたつもりでも)実態として net48 のままになっていれば、次のようなエラーになります。
- net8.0 をターゲットにした csproj は .NETFramework,Version=v4.8 をターゲットにするプロジェクトから参照できない
なぜ参照できないのか:TFM 互換性を「参照方向」で理解する
互換性で重要なのは「参照元(呼び出す側)」と「参照先(呼び出される側)」です。参照元の実行環境が参照先の API を理解できる必要があるため、一般に “古いランタイムは新しいランタイム向けの成果物を理解できない” という制約が出ます。
| 参照元(呼び出す側) | 参照先(ライブラリ) | プロジェクト参照 | 補足 |
|---|---|---|---|
| net48(.NET Framework 4.8) | net8.0(.NET 8) | 不可 | 今回のエラーの本質 |
| net8.0(.NET 8) | net48(.NET Framework 4.8) | 不可 | 逆方向も同様に不可。共存したいなら別案が必要 |
| net48(.NET Framework 4.8) | netstandard2.0 | 可 | 段階移行でよく使う“橋渡し” |
| net8.0(.NET 8) | netstandard2.0 | 可 | 共有ライブラリとして運用しやすい |
つまり、Folder3 から net8.0 の Folder / Folder1 / Folder2 を参照したいなら、Folder3 も net8.0(あるいは同等に互換性のあるフレームワーク)である必要があります。
「Folder3 も net8.0 に変えたつもり」なのに直らないときに疑うべきこと
現場で一番多いのは、csproj の移行が不完全で、Folder3 が実際には net48 として扱われ続けている パターンです。Visual Studio のプロパティ画面で “.NET 8” を選んだように見えても、プロジェクト ファイルやビルドの実体が追従していないことがあります。
まずは csproj を直接開いて、TFM を「文字列」で確認する
.NET 8 の SDK スタイル csproj は、基本的に次の形です。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
一方、.NET Framework の旧スタイル csproj には次のような記述が残っていることがあります(これが残っていると net48 扱いになりやすい)。
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
| 確認項目 | OKの例 | NGの例(残骸) | 起きがちなこと |
|---|---|---|---|
| csproj の先頭 | <Project Sdk=”…”> | <Project ToolsVersion=”…”> | 旧スタイルのまま TargetFramework だけ書き換えても完全移行にならない |
| ターゲット指定 | <TargetFramework>net8.0</TargetFramework> | <TargetFrameworkVersion>v4.8</TargetFrameworkVersion> | ビルドが net48 で評価され、参照エラーが消えない |
| 条件付き PropertyGroup | 単一で明確 | Configuration/Platform で分岐 | Debug だけ net48、Release だけ net8.0 など “片側だけ”残る |
| Directory.Build.props | 上書きなし | TargetFramework を強制 | ソリューション全体の共通設定が Folder3 を net48 に戻す |
「ビルド出力フォルダ」で最終判断する(いちばん確実)
ビルド後に生成されるフォルダが bin/Debug/net8.0/ なのか、bin/Debug/net48/ なのかを見れば、実際に何としてビルドされたかが一発で分かります。エラー文と合わせて、まずここで “現実” を確定させるのが近道です。
「別の csproj を参照している」事故を疑う
ソリューションが大きいと、似た名前のプロジェクトが複数あり、参照が想定と違う csproj を指していることがあります。例として、Folder3 から参照しているのが “移行後の net8.0 側” ではなく “移行前の net48 側の同名プロジェクト” になっていると、設定を変えても永遠に直りません。
- ソリューション エクスプローラーで参照先プロジェクトを右クリック → 「プロパティ」や「参照」を確認する
- 参照のパスが意図したフォルダの csproj を指しているか確認する(同名 csproj がないか)
- 古いプロジェクトが残っている場合は、リネームして区別する(例:ProjectName.Legacy)
コマンドで参照関係を確認する(GUIより迷わない)
CLI で参照関係を確かめると、Visual Studio の表示に引きずられずに確認できます。
dotnet list Folder3.csproj reference
参照が意図通りの csproj を指しているか、ここで確認すると「別物を直していた」事故を早期に潰せます。
解決策A:依存関係にある全プロジェクトを一気に net8.0 へ揃える
最もシンプルで、後戻りが少ないのが「一気に移行」パターンです。参照の互換性問題に悩まされにくく、最終形に近い状態でテストできます。
| 手順 | やること | コツ |
|---|---|---|
| 1 | 依存関係グラフを作る(どのプロジェクトがどれを参照しているか) | 参照の向き(呼び出し側 → 呼び出され側)を図にするだけで詰まりどころが見える |
| 2 | 最下層(ユーティリティ・ドメイン)から net8.0 へ変換 | 上位プロジェクトの修正量を減らすため、下から上へ |
| 3 | 中間層(サービス層)を net8.0 へ | DI(依存性注入)に寄せると後で ASP.NET Core と相性が良い |
| 4 | 最上位(Folder3 などのエントリポイント)を net8.0 へ | 最終的なホスティング形態(Web/Windows/Console)を先に決める |
ただし、一気に移行は「今すぐ直したい」より「全体最適」が目的です。既存運用の都合で共存期間が必要なら、次の段階移行が現実的です。
解決策B:段階移行で共存させる(.NET Standard / マルチターゲット)
Folder3(net48)を残しつつ、Folder / Folder1 / Folder2(net8.0)を使いたい場合、共有部分を netstandard2.0 で提供する か、マルチターゲット(net48;net8.0) にして “両方にビルドできるライブラリ” を作るのが定番です。
パターン1:共有ライブラリを netstandard2.0 に落とす(橋渡し用途)
「ロジックは共有したいが、最新 API に強く依存していない」なら、まずは netstandard2.0 が有力です。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
この形にすれば、net48 側からも net8.0 側からも参照できます。移行期間中は「ドメインモデル」「DTO」「共通ユーティリティ」などをここに寄せると、参照地獄になりにくいです。
注意点として、netstandard2.0 は “何でもできる” ではありません。UI 依存、Windows 専用 API、古い System.Web 系、特定のネイティブ依存などは持ち込みにくいので、純粋なビジネスロジック中心に寄せる のがコツです。
パターン2:マルチターゲットで net48 と net8.0 の両対応にする
「net48 では古い API を使い、net8.0 では新しい API を使いたい」「依存パッケージがフレームワーク別に分かれる」など、同じライブラリでもフレームワーク別に実装を分けたい ときはマルチターゲットが効きます。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
</PropertyGroup>
コード側は次のようにコンパイル条件で分岐できます。
#if NET48
// net48 向け実装
#else
// net8.0 向け実装
#endif
| 方式 | メリット | デメリット | 向いているケース |
|---|---|---|---|
| 一気に net8.0 へ統一 | 構成が単純、将来の保守が楽 | 同時に直す範囲が広い | リリースの自由度が高い、テスト体制がある |
| netstandard2.0 で橋渡し | 両陣営から参照できる、共有がしやすい | 使える API が制限される | ドメイン/DTO/共通処理を先に固めたい |
| マルチターゲット(net48;net8.0) | フレームワーク別に最適化できる | ビルドとテストが2系統になりがち | パッケージ制約や OS 依存が強い |
どうしても net48 と net8.0 を“同居”させたいときの第三の選択肢
プロジェクト参照で直接つなぐのが無理なら、設計として “プロセス境界” を作るのも実務的です。例えば、net8.0 側を別プロセス(Windows サービス、コンソール、Web API、gRPC など)として切り出し、net48 側は HTTP やメッセージングで呼び出します。移行期間を安全に伸ばせる反面、運用・監視・デプロイは増えるので、チームの状況に合わせて判断します。
それでもエラーが消えない場合の“切り分け”手順
移行中は「何が真実か」を決めるのが大事です。Visual Studio の表示よりも、ビルド時に評価された TargetFramework の値 を信じるのが安全です。
- bin/obj を削除してから再ビルドする(古い中間生成物が残っていると誤判定の原因になる)
- ソリューション全体の Directory.Build.props / Directory.Build.targets が TargetFramework を上書きしていないか確認する
- Debug/Release で TFM が分岐していないか(条件付き PropertyGroup)を確認する
- 参照の向きが正しいか(Folder3 → Folder/Folder1/Folder2)を再確認する
| 症状 | よくある原因 | まずやること |
|---|---|---|
| Folder3 を net8.0 にしたはずなのにエラー文が net48 のまま | csproj 移行漏れ、別 csproj を参照、共通 props で上書き | csproj を直接開き、TargetFramework の値を文字列で確認 |
| Debug だけ参照できない / Release だけ参照できない | 条件付き PropertyGroup で片方だけ net48 が残っている | Configuration/Platform ごとの設定を一度整理して統一 |
| CI では失敗、ローカルは成功 | SDK バージョン差、global.json、ビルドコマンド差 | CI とローカルで SDK を揃え、NuGet キャッシュも含めて再現性を上げる |
Upgrade Assistant で「単体コントローラー」がドロップダウンに出ない問題
Upgrade Assistant は便利ですが、万能ではありません。特に MVC / Web API の移行では、ツールが「候補として出す/出さない」を内部解析で判断するため、ユーザー側の感覚とズレが起きやすいです。
よくある前提:Upgrade Assistant は“プロジェクト単位”の移行が主軸
「Controller だけを選んで移行したい」というニーズは現場では多いのですが、Upgrade Assistant は基本的に プロジェクトが移行前(.NET Framework)なのか、移行後(.NET 8)なのか といった状態を見て、実行できるステップを変えます。
すでに API プロジェクト全体を .NET 8 に変換した後だと、ツール側が「そのステップは対象外」「すでに完了した扱い」と判断し、Controller の候補抽出をしない(またはスキップする)ことがあります。
| 状況 | 起きやすいこと | 現実的な対処 |
|---|---|---|
| 移行前の .NET Framework プロジェクトに対して実行 | 移行ステップが揃って表示されやすい | Upgrade Assistant の得意領域。まずここで全体移行の道筋を作る |
| すでに .NET 8 へ変換した後に “一部だけ” を再実行 | ドロップダウンに Controller が出ない/対象が少ない | 手動移植で進めるか、移行前の状態(別ブランチ)に対して再実行 |
候補に出すために満たしておきたい条件(最低限)
ツールが Controller と認識できる形に揃えると、候補に出る確率が上がります。
- クラスが public である
- クラス名が XxxController で終わっている
- 典型的な配置(例:Controllers フォルダ)で、プロジェクトに通常のコードファイルとして含まれている
- 解析時にプロジェクトが破綻していない(参照切れ・ビルド不能・依存パッケージ欠落があると候補抽出されにくい)
それでも出ない“実務で多い原因”
特定の Controller だけ出ない場合は、コードの形が一般的なパターンから外れている可能性があります。
- 部分クラス(partial)でファイルが分割されていて、解析が追いづらい
- Controller 名が規約外(例:XxxApi、BaseController だけ、末尾が Controller ではない)
- 内部クラス / ネストクラスになっている
- 古い Web API の基底(ApiController 等)を継承しつつ、周辺が独自拡張されている
- プロジェクトが一部でもビルド不能で、解析が中断されている(参照先 DLL が見つからない等)
現実解:Controller は手動移植で進める(移行速度を落とさない)
ツールにこだわり過ぎると、移行のボトルネックになります。Controller は差分が大きい領域なので、プロジェクトが .NET 8 側に立ち上がった時点で、Controller の移植は手動で淡々と進める 方が結果的に早いことが多いです。
.NET Framework Web API から ASP.NET Core への置き換え早見表
| 旧(.NET Framework / System.Web) | 新(ASP.NET Core / .NET 8) | 移植の要点 |
|---|---|---|
| ApiController | ControllerBase | [ApiController] 属性を付けるとバリデーションや 400 応答が自動化される |
| HttpResponseMessage | IActionResult / ActionResult<T> | return Ok(value), BadRequest(), NotFound() などに寄せる |
| System.Web.Http.RoutePrefix / Route | [Route] / [HttpGet] 等の属性ルーティング | Program.cs で MapControllers() を有効化 |
| GlobalConfiguration / WebApiConfig | Program.cs | ミドルウェアと DI で構成。Startup がないテンプレートも多い |
| ConfigurationManager.AppSettings | IConfiguration | appsettings.json + 環境変数などに統合しやすい |
| HttpContext.Current | ControllerBase.HttpContext | 静的アクセスをやめ、DI か引数で渡す方向へ |
移植例:典型的な Web API コントローラー
旧 Web API の例(概念)
using System.Net;
using System.Net.Http;
using System.Web.Http;
public class OrdersController : ApiController
{
[HttpGet]
public HttpResponseMessage Get(int id)
{
var order = ...;
if (order == null)
return Request.CreateResponse(HttpStatusCode.NotFound);
return Request.CreateResponse(HttpStatusCode.OK, order);
}
}
ASP.NET Core(.NET 8)側の例
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet("{id:int}")]
public ActionResult Get(int id)
{
var order = ...;
if (order == null)
return NotFound();
return Ok(order);
}
}
手動移植のチェックリスト(詰まりやすい順)
- DI(依存性注入):ServiceLocator 的な static 参照をやめ、コンストラクタ注入へ寄せる
- ルーティング:属性ルートに統一し、パラメータ制約({id:int})まで含めて意図を明確にする
- フィルタ:例外フィルタ / 認可フィルタ / アクションフィルタの実装が変わるため、最初に一覧化する
- HttpContext 周り:HttpContext.Current は存在しない。アクセスが必要なら IHttpContextAccessor の利用を検討
- JSON:既定は System.Text.Json。Newtonsoft.Json 依存が強い場合は切り替え方針を決める
- 認証・認可:OWIN/旧認証から ASP.NET Core の認証ハンドラへ。Cookie/JWT/OIDC など方式ごとに整理
- ModelState:[ApiController] では不正なモデルが自動的に 400 になる。既存のエラーハンドリングと衝突しないか確認
Upgrade Assistant を活かすためのおすすめ運用
Upgrade Assistant を最大限活かすコツは、ツールを「魔法の変換機」として扱わず、移行のガイドラインを作るための“解析・下準備ツール”として使うことです。
- 移行前の .NET Framework プロジェクトに対して実行し、変換対象・依存関係・問題点を洗い出す
- 移行後は、Controller やフィルタなど差分の大きい部分を手動で整える
- 「出ない」「できない」に時間を使うより、移植手順をテンプレ化して横展開する
移行全体をスムーズにする設計の考え方
今回の 2 つのトラブルは、別々に見えて共通点があります。それは “移行途中の状態(混在状態)”をどう設計するか です。混在期間を許容するなら、最初からそれに耐える構成を作る必要があります。
おすすめの分割:共有ライブラリとアプリを切り離す
段階移行をする場合は、次のように責務で分割しておくと参照方向が綺麗になります。
| 層 | ターゲットのおすすめ | 置くもの | 注意点 |
|---|---|---|---|
| 共有コア(Domain/DTO/Utility) | netstandard2.0(またはマルチターゲット) | 純粋なビジネスロジック、モデル、共通処理 | UI/DB/外部 API 依存を持ち込まない |
| インフラ層(DB/外部 API) | 移行状況に応じて net48 or net8.0 | リポジトリ、HTTP クライアント | 依存パッケージの TFM 制約が出やすい |
| アプリ層(Folder3 など) | 最終的に net8.0 | Web/Windows/Batch のエントリポイント | 一気に上げられないなら “橋渡し層” を先に作る |
まとめ:エラーを消す近道は「参照の整合性」を取ること
- .NET Framework 4.8 は net8.0 ライブラリを参照できない。参照エラーは仕様として発生する
- Folder3 を net8.0 にしたつもりでも、csproj の移行漏れや参照先の取り違えで net48 扱いが残ることがある
- 共存が必要なら、netstandard2.0 かマルチターゲットで “両方から呼べる” 形にしてから段階的に引き上げる
- Upgrade Assistant はプロジェクト単位が主軸。Controller 単体が出ないときは、移行前で実行するか、手動移植で先に進める

コメント