C#(WinForms)でWMIからCPUのProcessorIdとマザーボードのSerialNumberを取得してPC固有IDにしたいのに、usingで宣言した変数へ再代入しようとしてコンパイルエラーになる…という落とし穴は定番です。本記事では原因と、安全に書き直す実装例、現場でハマるポイントまでまとめます。
よくある要件:WMIで「PC固有ID」を作りたい
業務アプリや社内ツールで、次のような要件に出会うことがあります。
- 同じPCかどうかを判定したい(端末登録・端末認証・ライセンス管理など)
- OS再インストール後も、できれば同一端末として認識したい
- MACアドレスのように変更されやすい値ではなく、ハードウェア由来の値を使いたい
このとき候補に挙がりやすいのが、WMI(Windows Management Instrumentation)で取得できるハードウェア情報です。特に「CPUのProcessorId」と「マザーボードのSerialNumber」を連結して、端末識別子(フィンガープリント)にする方法はよく見かけます。
つまずきポイント:using変数へ再代入できない(cannot assign to using variable)
WMIの取得処理を書いていると、次のようなパターンになりがちです。
using System.Management;
using var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
using var objectList = searcher.Get();
// 何か処理…
// 次はマザーボードを取りたいので、同じ変数に入れ替えたい
objectList = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BaseBoard").Get(); // コンパイルエラー
ここで出るのが、「objectList は using 変数なので再代入できない」というコンパイルエラーです。言い換えると、usingで宣言したローカル変数はスコープ内で実質readonly扱いになり、別インスタンスを代入し直す書き方ができません。
using宣言とusingステートメント、どちらでも「再代入禁止」
C#のusingには大きく2種類の書き方があります。
| 書き方 | 例 | 破棄タイミング | 再代入 | 使いどころ |
|---|---|---|---|---|
| using宣言 | using var x = ...; | スコープ末尾 | 不可 | 短く書きたい/ネストを浅くしたい |
| usingステートメント | using (var x = ...) { ... } | ブロック末尾 | 不可 | 破棄範囲をブロックで明示したい |
どちらも「using変数(usingで宣言された変数)」という扱いになり、スコープ内で別のインスタンスを代入し直すことはできません。回避策として「usingを外して通常の変数にする」のは、次の理由でおすすめしません。
- WMI周りはCOMリソースを掴むことがあり、長時間動くアプリだとリークが積み重なる
- 破棄漏れのタイミングが読み取りづらく、後から保守する人が困る
なぜreadonly扱いになるのか
usingは「スコープを抜けるときに必ずDispose()を呼んで破棄する」という保証を与える構文です。もし途中で参照先を自由に入れ替えられると、最初に作ったインスタンスの破棄が曖昧になり、破棄漏れ(リソースリーク)を起こしやすくなります。これを防ぐために、コンパイラがusing変数への再代入を禁止しています。
イメージとしては、次のようなtry/finallyに展開されるのを想像すると理解しやすいです。
var objectList = searcher.Get();
try
{
// ここで objectList を別のものに代入できたら…
}
finally
{
objectList.Dispose(); // どれを破棄するの?が曖昧になる
}
正攻法:破棄対象ごとにusingを分ける(必要ならネストする)
解決策はシンプルで、CPU取得用のusingとマザーボード取得用のusingを分けます。破棄が必要なオブジェクト(ManagementObjectSearcherやManagementObjectCollectionなど)を「その場で作って、その場で破棄する」形にすると、安全で読みやすくなります。
まずは最小構成の例(CPU→BaseBoardの順で取得)
WMIの結果コレクションを別変数に保持するのではなく、必要な値(文字列)だけを抜き出して返す形にすると、Disposeの範囲がはっきりし、再代入問題とも無縁になります。
using System;
using System.Management;
public static class PcIdProvider
{
public static string GetPcIdRaw()
{
var cpuId = GetFirstWmiValue(
"SELECT ProcessorId FROM Win32_Processor",
"ProcessorId");
var boardSerial = GetFirstWmiValue(
"SELECT SerialNumber FROM Win32_BaseBoard",
"SerialNumber");
// 連結ルールは固定しておく(後で仕様変更すると照合できなくなるため)
return $"CPU={cpuId}|BOARD={boardSerial}";
}
private static string GetFirstWmiValue(string query, string propertyName)
{
// 例外が出る可能性があるので、実運用ではログ出力も検討
try
{
using var searcher = new ManagementObjectSearcher(query);
using var results = searcher.Get();
foreach (ManagementObject mo in results)
{
using (mo)
{
var value = mo[propertyName];
var text = Convert.ToString(value)?.Trim();
if (!string.IsNullOrEmpty(text))
{
return text;
}
}
}
}
catch (ManagementException)
{
// WMIクラス名/プロパティ名の誤り、WMI側エラーなど
}
catch (UnauthorizedAccessException)
{
// 権限不足(環境によっては管理者権限が必要になることがある)
}
catch (System.Runtime.InteropServices.COMException)
{
// WMIサービスやCOM周りの問題
}
return "";
}
}
ポイントは、WMIクエリごとにusing var searcherとusing var resultsを作って閉じていることです。これで「using変数へ再代入できない」問題を回避しながら、リソースも確実に破棄できます。
同じ変数名を使いたいなら「ブロックでスコープを切る」
「結果コレクションの変数名は毎回resultsにしたい」などの好みがある場合は、{ }でスコープを分ければ再宣言できます。using宣言はスコープ末尾で破棄されるため、ブロックを切ると意図がさらに明確になります。
using System;
using System.Management;
string cpuId;
{
using var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
using var results = searcher.Get();
cpuId = "";
foreach (ManagementObject mo in results)
{
using (mo)
{
cpuId = Convert.ToString(mo["ProcessorId"])?.Trim() ?? "";
if (!string.IsNullOrEmpty(cpuId)) break;
}
}
}
string boardSerial;
{
using var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BaseBoard");
using var results = searcher.Get();
boardSerial = "";
foreach (ManagementObject mo in results)
{
using (mo)
{
boardSerial = Convert.ToString(mo["SerialNumber"])?.Trim() ?? "";
if (!string.IsNullOrEmpty(boardSerial)) break;
}
}
}
「usingを外すと例外になる」もう一つの典型:破棄された後に列挙している
「usingを外したら例外が減った/増えた」と感じるケースはありますが、原因はusingそのものではなく、破棄タイミングと列挙タイミングのズレであることもあります。
例えば次のように、usingブロックを抜けた後に結果を列挙していると、環境によっては不安定になります(WMIの裏側はCOM列挙子を使うため、破棄順序が影響することがあります)。
ManagementObjectCollection results;
using (var searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor"))
{
results = searcher.Get();
} // ここで searcher が破棄される
// 後から results を列挙(環境によっては不安定になりやすい)
foreach (ManagementObject mo in results)
{
// ...
}
安全策としては、取得(Get)と列挙(foreach)を同じusingスコープ内に閉じ込めることです。先ほどのGetFirstWmiValueのように「値だけ取り出して返す」形にすると、この手の事故を避けやすくなります。
WMIクエリのクラス名/プロパティ名を間違えると実行時例外になりやすい
実行時例外の原因として非常に多いのが、WMIのクラス名やプロパティ名のミスです。例えば、CPUのProcessorIdは通常次で取得します。
- クラス:
Win32_Processor - プロパティ:
ProcessorId
クラス名は大文字小文字が違っても通ることがありますが、そもそも存在しないクラス名(例:Win32_processorIDのような「それっぽいが実在しない名前」)だとManagementExceptionなどが発生します。まずはクエリを見直すのが近道です。
よく使うWMIクラスとプロパティ一覧
| 目的 | WMIクラス | 代表的なプロパティ | クエリ例 | 注意点 |
|---|---|---|---|---|
| CPUのID | Win32_Processor | ProcessorId | SELECT ProcessorId FROM Win32_Processor | 環境により空文字や同一値のことがある |
| マザーボードのシリアル | Win32_BaseBoard | SerialNumber | SELECT SerialNumber FROM Win32_BaseBoard | メーカー実装によって「To Be Filled By O.E.M.」などになることがある |
| BIOSのシリアル | Win32_BIOS | SerialNumber | SELECT SerialNumber FROM Win32_BIOS | BaseBoardが空のときの代替候補 |
| システムUUID | Win32_ComputerSystemProduct | UUID | SELECT UUID FROM Win32_ComputerSystemProduct | 仮想環境ではテンプレート値になることも |
「CPU+BaseBoard」でうまく取れない端末がある場合、BIOS SerialNumberやUUIDを“保険”として追加し、空のときにフォールバックする設計にすると運用が安定します。
参照追加:System.Management を使えるようにする
WMIを扱うにはSystem.Management名前空間を利用します。プロジェクト種別によって追加方法が違うため、ここもつまずきポイントになりがちです。
| 環境 | 追加方法の例 | 補足 |
|---|---|---|
| .NET Framework(WinForms) | 参照に「System.Management」を追加 | 古いプロジェクトでは既に参照済みのこともあります |
| .NET(Core/5+)のWinForms | NuGetで「System.Management」を追加 | Windows専用APIのため、Windows以外では動きません |
CLI派なら次のコマンドで追加できます。
dotnet add package System.Management
実務でハマりやすいポイントと、落ちにくい実装のコツ
複数返ってくるのに、最後の要素で上書きしてしまう
WMIの結果は1件とは限りません。例えばCPUが複数ソケットの端末や、仮想環境の構成によっては複数行返ることがあります。
よくある落とし穴が「foreachで回して、変数に代入し続け、結果的に最後の要素が残る」パターンです。1件だけ欲しいなら、最初の有効値でbreakして、意図をコードに表してください。
foreach (ManagementObject mo in results)
{
using (mo)
{
var v = Convert.ToString(mo["ProcessorId"])?.Trim();
if (!string.IsNullOrEmpty(v))
{
cpuId = v;
break; // ここが重要
}
}
}
obj[“…”] はnullになり得る(DBNull相当もあり得る)
WMIのプロパティは端末・メーカー・BIOS実装次第で空だったり、そもそも値が返らなかったりします。ToString()直呼びで落ちるのを避けるために、Convert.ToStringや?.でnull安全にしておくと、サポート対応が楽になります。
管理者権限が必要になるケースがある
多くの端末では一般ユーザー権限でも取得できますが、グループポリシーやセキュリティ製品の設定、WMI名前空間のアクセス制御によってはUnauthorizedAccessExceptionが出ます。現場では「特定部署のPCだけ落ちる」「マスターイメージだけ取得できない」などで発覚しがちです。
- まずはWMIサービス(Windows Management Instrumentation)が停止していないか確認
- 端末管理ポリシーでWMIアクセスが制限されていないか確認
- 必要なら管理者権限で実行する、または取得項目を見直す
WinFormsのUIが固まる(WMIは意外と遅い)
WMIは問い合わせに時間がかかることがあります。ボタンクリックなどUIスレッド上で実行すると、一瞬アプリが固まったように見えることもあります。端末識別子を表示するだけでも、体感が悪くなるならバックグラウンドで取得するのが無難です。
using System.Threading.Tasks;
private async void btnGetId_Click(object sender, EventArgs e)
{
btnGetId.Enabled = false;
try
{
var id = await Task.Run(() => PcIdProvider.GetPcIdRaw());
txtPcId.Text = id;
}
finally
{
btnGetId.Enabled = true;
}
}
より現場向け:空値や“それっぽいダミー値”を除外して組み立てる
マザーボードのSerialNumberが「To Be Filled By O.E.M.」のような“ダミー文字列”になるメーカーがあります。このまま連結すると、同一文字列同士で衝突する危険が出ます。最低限、空文字だけでなく、明らかにダミーと分かる値は除外しておくと安全です。
using System;
private static bool IsUseless(string? text)
{
if (string.IsNullOrWhiteSpace(text)) return true;
var t = text.Trim();
// よくあるダミー値(環境に合わせて追加)
if (t.Equals("To Be Filled By O.E.M.", StringComparison.OrdinalIgnoreCase)) return true;
if (t.Equals("Default string", StringComparison.OrdinalIgnoreCase)) return true;
if (t.Equals("None", StringComparison.OrdinalIgnoreCase)) return true;
if (t.Equals("System Serial Number", StringComparison.OrdinalIgnoreCase)) return true;
return false;
}
そして組み立て側では、使える値だけを集めて固定ルールで連結します。
using System.Collections.Generic;
public static string GetPcIdRawStable()
{
var parts = new List<string>();
var cpu = GetFirstWmiValue("SELECT ProcessorId FROM Win32_Processor", "ProcessorId");
if (!IsUseless(cpu)) parts.Add("CPU=" + cpu);
var board = GetFirstWmiValue("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber");
if (!IsUseless(board)) parts.Add("BOARD=" + board);
// 保険:上2つが取れない端末のために追加
var bios = GetFirstWmiValue("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber");
if (!IsUseless(bios)) parts.Add("BIOS=" + bios);
var uuid = GetFirstWmiValue("SELECT UUID FROM Win32_ComputerSystemProduct", "UUID");
if (!IsUseless(uuid)) parts.Add("UUID=" + uuid);
return string.Join("|", parts);
}
そのまま保存せず、ハッシュ化して扱うと運用が楽
ProcessorIdやSerialNumberは、端末の内部識別情報としては便利ですが、アプリ側でそのまま保存・送信すると取り扱いが気になる場面があります。例えばログに残る、問い合わせ時にスクリーンショットで見えてしまう、などです。
端末照合の目的なら、元値を隠す意味でもSHA-256などでハッシュ化しておくと扱いやすくなります。さらに、プロダクトごとに固定のソルト(秘密値)を混ぜると、別製品間で同じ端末IDが流用されにくくなります。
using System.Security.Cryptography;
using System.Text;
public static string ToSha256(string input)
{
using var sha = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha.ComputeHash(bytes);
var sb = new StringBuilder(hash.Length * 2);
foreach (var b in hash)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
// 利用例
var raw = PcIdProvider.GetPcIdRawStable();
var salt = "YourProductFixedSalt";
var fingerprint = ToSha256(salt + "|" + raw);
トラブルシューティング:失敗パターンと切り分け
| 症状 | よくある原因 | 対策 |
|---|---|---|
| コンパイルで「cannot assign to using variable」 | using宣言した変数に再代入している | クエリごとにusingを分ける/別変数名にする/ブロックでスコープを切る |
ManagementExceptionが出る | クラス名・プロパティ名の誤り、WMI側エラー | クエリを見直す(Win32_Processor, Win32_BaseBoardなど)/例外内容をログに残す |
UnauthorizedAccessExceptionが出る | 権限不足、ポリシーでWMIアクセス制限 | 管理者権限で検証/取得項目変更/端末管理ポリシー確認 |
| 値が空、またはダミーっぽい文字列 | メーカー実装、BIOS情報が未設定 | ダミー値を除外/BIOS SerialやUUIDにフォールバック |
| UIが固まる | WMI取得が遅い(UIスレッドで実行) | Task.Runでバックグラウンド実行/起動時に非同期で取得してキャッシュ |
運用面の注意:ハード交換・クローン・仮想環境
「PC固有ID」と言っても、現実には絶対に変わらない値を保証するのは難しいです。CPU交換やマザーボード交換で変わるのはもちろん、仮想環境ではテンプレート由来の値になって衝突することもあります。
そのため、ライセンスや端末登録の設計では次のような方針を持つと、後で詰まりにくくなります。
- 端末IDは“完全一致のみ”に頼り切らず、再認証・再登録の逃げ道を用意する
- WMI値は空やダミーがあり得る前提で、複数ソースから組み立てる
- 連結ルール(区切り文字、正規化の仕方)を固定し、後から変えない
- 必要以上に生の値を露出させず、ハッシュ化して保存・送信する
まとめ:using再代入は避け、WMI取得処理は「小さく閉じる」
usingで宣言した変数に再代入できないのは、Disposeを確実にするための仕様です。WMI取得では、クエリごとにusingを分けてスコープを小さく閉じるのが、最も安全で読みやすい解決策になります。
さらに、WMIクエリのクラス名・プロパティ名の正しさ、空値・ダミー値への耐性、UIフリーズ対策、ハッシュ化などを押さえると、ProcessorId+SerialNumberでのPC識別が現場で使える品質に近づきます。

コメント