「この端末に最初に入ったインストールか? それとも再インストールか?」――.NET MAUIでこの要件を確実に満たすには、OSごとに異なる識別子の性質を理解し、永続化の落とし穴を避ける設計が必要です。本稿はAndroid/iOS/Windows/Tizen/MacCatalystの挙動差を踏まえ、実務で再利用できるコードと判定フローをまとめました。
.NET MAUIで「初回インストール判定」を正しく設計するには
結論から先に整理します。クロスプラットフォームで「完全に不変なデバイスID」を公式APIだけで実現することはできません。ゆえに、OSが提供する最良の識別子と、各OSの最も安定した永続化手段を組み合わせ、さらにサーバー側の補助で再インストールを同定するのが現実解です。
- Android:
Settings.Secure.AndroidIdが最適。工場出荷状態リセットやユーザー切替で変わるが、アンインストール→再インストールでは変わらない。 - iOS / MacCatalyst:
IdentifierForVendorは全ベンダーアプリ削除でリセット。再インストールでも値が変わり得るため、Keychainに自前GUIDを保存して代用するのが実務的。 - Windows:
Windows.System.Profile.SystemIdentification.GetSystemIdForPublisher()が安定。アプリ再インストール後も同一IDを得やすい。 - Tizen:公式の端末一意IDは提供されないため、SecureStorageにGUIDを保存。
プラットフォーム別:取得できるIDと注意点
| プラットフォーム | 取得コード(例) | 性質・注意点 | 初回判定への適性 |
|---|---|---|---|
| Android | #if ANDROID using Android.Provider; using Android.App; string deviceId = Settings.Secure.GetString( Application.Context.ContentResolver, Settings.Secure.AndroidId); #endif | 端末初回起動時に生成。再インストールでは変わらない。 工場出荷状態リセット/マルチユーザー切替で変化。 広告IDではない(追跡許諾不要)。 | ◎(サーバーと組み合わせれば再インストール判定に最適) |
| iOS | #if IOS using UIKit; string idfv = UIDevice.CurrentDevice.IdentifierForVendor?.ToString(); #endif | 同一ベンダーの全アプリを削除するとリセット。 アンインストール→再インストールで変わる可能性がある。 端末一意IDとしては不十分。Keychainに自前GUIDを保存して補完。 | △(Keychain GUIDとの併用が前提) |
| Windows | #if WINDOWS using Windows.System.Profile; using Windows.Security.Cryptography; var sysId = SystemIdentification.GetSystemIdForPublisher(); byte[] bytes; CryptographicBuffer.CopyToByteArray(sysId.Id, out bytes); string deviceId = Convert.ToBase64String(bytes); #endif | ハードウェアに結び付く識別子。仮想環境では弱くなる場合あり。 MACアドレス連結は推奨しない(権限・仮想NIC・変更リスク)。 | ◎(推奨APIで安定) |
| MacCatalyst | // 公式の端末一意IDはなし。KeychainにGUIDを保存して利用。 | SecureStorageはアンインストールで消える。 Keychainは多くのケースで残る(将来変更の可能性に注意)。 | ◯(Keychain GUID前提) |
| Tizen | // 一意IDの公式APIは想定しない。 // SecureStorageにGUIDを保存。 | アンインストールで消えるため、サーバー補助が前提。 | ◯(要サーバー補助) |
「初回インストールか?」を判定するアーキテクチャ
ローカルのみで判定(最小構成)
手軽なのはSecureStorageにフラグ/GUIDを保存する方法です。初回起動時に値がなければ生成して保存、以後は存在確認で「初回でない」と判定します。
const string KeyId = "CrossPlatformDeviceId";
var storedId = await SecureStorage.Default.GetAsync(KeyId);
bool isFirstInstall = storedId is null;
if (isFirstInstall)
{
storedId = Guid.NewGuid().ToString("N");
await SecureStorage.Default.SetAsync(KeyId, storedId);
}
ただしアンインストールでSecureStorageは消えるため、「同一端末での再インストール」を区別できません。これを必要とする要件では次のいずれかが必要です。
- iOS/MacCatalyst:KeychainにGUIDを保存(多くの場合アンインストール後も残る)。
- Android/Windows:安定したOS提供ID(AndroidId/SystemIdentification)を用いてサーバーで照合。
サーバー補助で判定(実務向け)
アプリは「候補となるdeviceId」をサーバーに送信し、サーバーは初見のID=初回インストール、既存ID=再インストールとして扱います。サーバー側でdeviceId→インストール履歴を保持する発想です。
- Android:
AndroidIdをdeviceIdに採用(高い再現性)。 - iOS / MacCatalyst:Keychain GUID(永続)を第一候補、fallbackに
IdentifierForVendor。 - Windows:
SystemIdentificationをdeviceIdに採用。 - Tizen:SecureStorage GUID(ローカルは消えるため、ログインIDなど他要素と組み合わせる)。
iOS/MacCatalyst:Keychainに自前GUIDを保存する
Keychainはアプリ削除後もエントリが残る挙動が一般的です(復元や端末初期化、アクセスグループ変更などで失われる可能性はあります)。以下はkSecAttrAccessibleAfterFirstUnlock(.NETではSecAccessible.AfterFirstUnlock)で保存する例です。
#if IOS || MACCATALYST
using Security;
using Foundation;
static class KeychainDeviceId
{
const string Service = "com.example.maui.deviceid";
const string Account = "device";
public static string GetOrCreate()
{
var query = new SecRecord(SecKind.GenericPassword)
{
Service = Service,
Account = Account
};
var record = SecKeyChain.QueryAsRecord(query, out var status);
if (status == SecStatusCode.Success && record?.ValueData != null)
{
return NSString.FromData(record.ValueData, NSStringEncoding.UTF8);
}
var newId = Guid.NewGuid().ToString("N");
var data = NSData.FromString(newId, NSStringEncoding.UTF8);
var addRec = new SecRecord(SecKind.GenericPassword)
{
Service = Service,
Account = Account,
ValueData = data,
Accessible = SecAccessible.AfterFirstUnlock
};
var addStatus = SecKeyChain.Add(addRec);
if (addStatus != SecStatusCode.Success && addStatus != SecStatusCode.DuplicateItem)
throw new Exception($"Keychain add failed: {addStatus}");
return newId;
}
}
#endif
このGUIDをdeviceIdとしてサーバーへ送れば、同一端末での再インストールを高精度に同定できます。
Windows:SystemIdentificationを使う
WindowsではSystemIdentification.GetSystemIdForPublisher()が最も確実です。MAUI(WinUI 3)でも#if WINDOWSで分岐して呼び出せます。
#if WINDOWS
using Windows.System.Profile;
using Windows.Security.Cryptography;
static string GetWindowsDeviceId()
{
var sysId = SystemIdentification.GetSystemIdForPublisher();
if (sysId == null || sysId.Id == null) return string.Empty;
CryptographicBuffer.CopyToByteArray(sysId.Id, out var bytes);
return Convert.ToBase64String(bytes); // 送信用にBase64化
}
#endif
従来よく見かける「MACアドレス連結」方式は、権限・仮想NIC・ハード交換で破綻しやすく、再現性も管理コストも悪いため避けるべきです。
Android:AndroidIdを使う
#if ANDROID
using Android.Provider;
using Android.App;
static string GetAndroidDeviceId()
{
return Settings.Secure.GetString(
Application.Context.ContentResolver,
Settings.Secure.AndroidId);
}
#endif
AndroidIdはユーザー/工場出荷状態リセットで変化し得ますが、アンインストール→再インストールでは同一であるため、サーバー側に記録しておけば「再インストール」を高確度に見分けられます。
クロスプラットフォーム実装:IDプロバイダと初回判定サービス
MAUIでの再利用性を高めるため、IDの採取と初回判定の永続化を分離します。以下は実務ですぐ差し込める構造です。
インターフェイス
public interface IDeviceIdentifier
{
/// <summary>端末を最も安定的に表すID(best-effort)。空文字なら利用不可。</summary>
Task<string> GetStableDeviceIdAsync();
/// <summary>この端末で当アプリが初めて起動したか(best-effort)。</summary>
Task<bool> IsFirstInstallAsync();
}
共通(デフォルトの初回判定ロジック)
public class DefaultDeviceIdentifier : IDeviceIdentifier
{
const string MarkerKey = "install-marker";
const string GuidKey = "install-guid";
public virtual async Task<string> GetStableDeviceIdAsync()
{
// 既存GUID(SecureStorage)を再利用
var id = await SecureStorage.Default.GetAsync(GuidKey);
if (!string.IsNullOrWhiteSpace(id)) return id;
// 取得できない場合は生成して保存(iOS/Mac以外はアンインストールで消える前提)
id = Guid.NewGuid().ToString("N");
await SecureStorage.Default.SetAsync(GuidKey, id);
return id;
}
public virtual async Task<bool> IsFirstInstallAsync()
{
var marker = await SecureStorage.Default.GetAsync(MarkerKey);
if (marker is null)
{
await SecureStorage.Default.SetAsync(MarkerKey, DateTime.UtcNow.ToString("O"));
return true;
}
return false;
}
}
Android固有の安定IDを上書き
#if ANDROID
public class AndroidDeviceIdentifier : DefaultDeviceIdentifier
{
public override Task<string> GetStableDeviceIdAsync()
{
var id = Settings.Secure.GetString(
Application.Context.ContentResolver,
Settings.Secure.AndroidId);
return Task.FromResult(id ?? string.Empty);
}
}
#endif
iOS / MacCatalyst:KeychainのGUIDを利用
#if IOS || MACCATALYST
public class AppleDeviceIdentifier : DefaultDeviceIdentifier
{
public override Task<string> GetStableDeviceIdAsync()
{
var id = KeychainDeviceId.GetOrCreate(); // 前掲Keychainヘルパー
return Task.FromResult(id);
}
}
#endif
Windows固有の安定IDを上書き
#if WINDOWS
public class WindowsDeviceIdentifier : DefaultDeviceIdentifier
{
public override Task<string> GetStableDeviceIdAsync()
{
var id = GetWindowsDeviceId(); // 前掲ヘルパー
return Task.FromResult(id ?? string.Empty);
}
}
#endif
DI登録(MauiProgram)
builder.Services.AddSingleton<IDeviceIdentifier>(sp =>
{
#if ANDROID
return new AndroidDeviceIdentifier();
#elif IOS || MACCATALYST
return new AppleDeviceIdentifier();
#elif WINDOWS
return new WindowsDeviceIdentifier();
#else
return new DefaultDeviceIdentifier(); // Tizen等
#endif
});
起動時の利用例
public partial class App : Application
{
public App(IDeviceIdentifier deviceIdentifier)
{
InitializeComponent();
MainPage = new AppShell();
Task.Run(async () =>
{
var deviceId = await deviceIdentifier.GetStableDeviceIdAsync();
var isFirst = await deviceIdentifier.IsFirstInstallAsync();
// サーバー通知の例
// await Api.ReportInstallAsync(new { deviceId, isFirst });
});
}
}
再インストールをより確度高く見分けるサーバー側の設計
| 送信フィールド | 用途 | 備考 |
|---|---|---|
| deviceId | 端末の主キー候補 | Android=AndroidId、iOS/Mac=Keychain GUID、Windows=SystemIdentification |
| appInstanceId | アプリインスタンス識別 | 毎インストールで生成するGUID(SecureStorage)。アンインストールで変わる。 |
| appVersion / osVersion | 分解能向上 | アップデート/再インストールの傾向分析に有用。 |
| firstLaunchAt | 初回起動タイムスタンプ | クライアントで保存した初回起動日時を送る。 |
サーバーは以下の判定ルールを推奨します。
- deviceIdが新規:初回インストール。
- deviceIdが既存かつappInstanceIdが新規:同一端末での再インストール。
- deviceIdが空(取得できず)か不安定:ログインIDや端末属性のハッシュで補助判定。
プライバシーと審査に関する重要注意
- iOS:ユーザー追跡を目的とした恒久IDの収集は厳格に制限。広告ID(IDFA)は許可なく利用不可。IdentifierForVendorは再インストールで変わり得るため、端末一意IDの代替には不適。
- Android:
AndroidIdは広告IDと異なり許諾不要だが、目的を明確にし、プライバシーポリシーに記載するのが実務上の必須要件。 - Windows:企業環境(MDM・仮想化)ではIDの粒度が変わる場合あり。規約・社内ポリシーに従う。
- MACアドレス・IMEIの取得は避ける:技術的にも審査的にも非推奨/取得不可が多い。
落とし穴と対処法(チェックリスト)
- アプリ削除でSecureStorageが消える:iOS/MacはKeychain、他OSはサーバー側照合で補完。
- iOSで同一ベンダーアプリ全削除→IDFVリセット:依存しない。Keychain GUIDを使う。
- Androidのマルチユーザー:ユーザーごとにAndroidIdが異なる可能性。要件に合わせて許容する。
- Windowsの仮想化・リイメージ:SystemIdentificationの強度が環境に依存。業務要件に合わせてバックアップキー(ユーザー認証)を併用。
- オフライン初回起動:サーバー登録は後送で整合。firstLaunchAtを添えて補正。
比較表:方式ごとの永続性・再現性・コスト
| 方式 | 永続性(再インストール) | 再現性 | 実装コスト | 備考 |
|---|---|---|---|---|
| SecureStorageのみ | ×(消える) | ◯ | 低 | 初回判定のみなら可。再インストール検知は不可。 |
| iOS Keychain GUID | ◯(多くのケースで残る) | ◯ | 中 | 将来変更リスクあり。アクセスグループ設定に注意。 |
| AndroidId | ◯(再インストールで同一) | ◯ | 低 | 工場出荷状態リセットで変化。 |
| Windows SystemIdentification | ◯ | ◯ | 低 | 仮想環境では弱くなることがある。 |
| MACアドレス連結 | △ | × | 高 | 非推奨。権限・NIC変動・取得不可環境が多い。 |
ユースケース別の実装ガイド
「初回インストール判定だけで良い」
- 全OS共通で
SecureStorageにインストールマーカーを保存。 - 再インストールを別端末扱いして良い要件ならこれで完了。
「同一端末の再インストールを区別したい」
- Android:
AndroidIdをdeviceIdに、appInstanceIdは毎回新規発行。 - iOS / MacCatalyst:Keychain GUIDをdeviceIdに。
- Windows:
SystemIdentificationをdeviceIdに。 - サーバー側でdeviceId×appInstanceIdの履歴を保持し、同一deviceIdでappInstanceIdが更新されたら「再インストール」。
「完全オフラインの業務端末」
- iOS/MacはKeychain、Android/WindowsはOS提供IDを使い、ローカルDB(SQLite)に履歴を保存。
- 端末初期化時に履歴が消えることを受け入れる設計に。
テスト手順(チェックリスト)
- 初回起動で
IsFirstInstallAsync()がtrueになること。 - アプリを終了→再起動で
falseになること。 - アンインストール→再インストール後:
- Android:
AndroidIdが同一であること。 - iOS/Mac:Keychain GUIDが同一であること(IDFVは変わり得る)。
- Windows:
SystemIdentificationが同一であること。
- Android:
- 端末初期化(工場出荷状態)後はdeviceIdが変わることを確認。
- サーバー照合で、deviceId既存+appInstanceId新規を「再インストール」と認識すること。
FAQ(よくある質問)
Q. IdentifierForVendor(iOS)だけで足りますか?
A. いいえ。ベンダーの全アプリ削除でリセットされ、再インストールでも変わり得ます。Keychain GUIDとの併用が実務的です。
Q. Keychainは本当にアンインストール後も残りますか?
A. 多くのケースで残りますが、OSの将来変更やアクセスグループ設定、端末の消去・復元で失われる可能性があります。サーバー側の補助ロジックを併用してください。
Q. MACアドレスやIMEIを使えますか?
A. 非推奨(あるいは取得不可)です。審査・権限・実機差のリスクが高く、保守コストも大きくなります。
Q. オフライン環境でも再インストールを見分けたいです。
A. iOS/MacはKeychain、Android/WindowsはOS提供ID+ローカルDBで履歴管理。ただし端末初期化などで完全な保証はできません。
最小実装スニペット集(コピペ可)
AndroidIdの取得
#if ANDROID
using Android.Provider;
using Android.App;
string androidId = Settings.Secure.GetString(
Application.Context.ContentResolver,
Settings.Secure.AndroidId);
#endif
iOS/ MacCatalyst Keychain GUID
// KeychainDeviceId.GetOrCreate() を利用(前掲)
string deviceId = KeychainDeviceId.GetOrCreate();
Windows SystemIdentification
#if WINDOWS
string deviceId = GetWindowsDeviceId();
#endif
初回インストール判定(共通)
const string MarkerKey = "install-marker";
bool isFirst = (await SecureStorage.Default.GetAsync(MarkerKey)) is null;
if (isFirst)
await SecureStorage.Default.SetAsync(MarkerKey, DateTime.UtcNow.ToString("O"));
設計指針のまとめ
- Android:
AndroidIdをdeviceIdに採用。 - iOS / MacCatalyst:Keychain GUIDをdeviceIdに。
IdentifierForVendorは補助。 - Windows:
SystemIdentificationをdeviceIdに。 - Tizen:SecureStorage GUID+サーバー照合。
- 「初回インストールだけ判定」で良ければSecureStorageのみでシンプルに。
- 「再インストール検知」まで必要なら、deviceId × appInstanceIdでサーバー履歴管理。
- プライバシーポリシー整備と、用途の明確化(内部分析・不正対策など)。
付録:ハードな要件に対する妥協点
「法務・審査・コストを踏まえ、どこまでを ‘同一端末’ とみなすか」を最初に合意しましょう。例えば、工場出荷状態リセットやMDM端末の再プロビジョニングまで同一端末とみなすのは技術的に困難です。現実的には次の粒度で線引きします。
- 再インストール:アプリを消して入れ直しただけ → 同一端末扱い(AndroidId/Keychain/SystemIdentificationで判別)。
- 端末初期化・ユーザー切替:別端末扱い(IDが変わる可能性を許容)。
この設計で得られるもの
- OSごとの仕様差に依存し過ぎず、ビジネス要件に合わせた一貫動作。
- サーバー履歴を用いた再インストールの高精度検出。
- プライバシーと審査への配慮を前提にした持続可能な運用。
これらを踏まえれば、MAUIアプリでも「初回インストール判定」と「再インストール検知」を堅実に実装できます。
結論
クロスプラットフォームで不変な端末IDは「存在しない」。だからこそ、AndroidはAndroidId、iOS/MacはKeychain GUID、WindowsはSystemIdentificationというOS最適解の組み合わせと、サーバー履歴で再インストールを見極めるのが最良手です。初回判定のみならSecureStorageで十分。要件に応じて、正しいトレードオフを選択しましょう。

コメント