C# WinFormsでWMIのProcessorIdとSerialNumberを取得する方法:using変数へ再代入できない原因と解決策

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のIDWin32_ProcessorProcessorIdSELECT ProcessorId FROM Win32_Processor環境により空文字や同一値のことがある
マザーボードのシリアルWin32_BaseBoardSerialNumberSELECT SerialNumber FROM Win32_BaseBoardメーカー実装によって「To Be Filled By O.E.M.」などになることがある
BIOSのシリアルWin32_BIOSSerialNumberSELECT SerialNumber FROM Win32_BIOSBaseBoardが空のときの代替候補
システムUUIDWin32_ComputerSystemProductUUIDSELECT UUID FROM Win32_ComputerSystemProduct仮想環境ではテンプレート値になることも

「CPU+BaseBoard」でうまく取れない端末がある場合、BIOS SerialNumberやUUIDを“保険”として追加し、空のときにフォールバックする設計にすると運用が安定します。

参照追加:System.Management を使えるようにする

WMIを扱うにはSystem.Management名前空間を利用します。プロジェクト種別によって追加方法が違うため、ここもつまずきポイントになりがちです。

環境追加方法の例補足
.NET Framework(WinForms)参照に「System.Management」を追加古いプロジェクトでは既に参照済みのこともあります
.NET(Core/5+)のWinFormsNuGetで「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識別が現場で使える品質に近づきます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次