Scanning, Connecting & the Current Network
IWifiManager covers the station side of the radio: what is in range, what you are joined to, and
moving between the two.
Scanning
Section titled “Scanning”var access = await wifi.RequestAccess(ct);if (access != AccessState.Available) return;
var networks = await wifi.Scan(ct);Results come back strongest first, one entry per BSSID — not per SSID. A multi-band router or a mesh network answers on several radios, so the same name appears more than once. Group them if you are building a picker:
var forDisplay = networks .GroupBy(x => x.Ssid) .Select(g => g.MaxBy(x => x.SignalStrengthPercent)!) .Where(x => !x.IsHidden) .ToList();WifiNetwork
Section titled “WifiNetwork”| Member | Notes |
|---|---|
Ssid |
Empty for a hidden network that did not broadcast one |
Bssid |
The MAC of the radio that answered; null where the platform withholds it |
Security |
Open, Wep, WpaPsk, Wpa2Psk, Wpa3Psk, Enterprise, Owe, Unknown |
SignalStrengthDbm |
Typically -30 (excellent) to -90 (unusable). Null on Linux, which reports only a quality percentage |
SignalStrengthPercent |
0-100, populated on every platform — the safe one to display |
FrequencyMhz |
Centre frequency |
Band / Channel |
Derived from the frequency |
IsHidden |
The access point does not broadcast its SSID |
IsOpen |
No passphrase needed. WEP counts as secured here, but treat it as open in practice |
iOS has no scan
Section titled “iOS has no scan”There is no public API. The only thing that lists nearby networks is NEHotspotHelper, whose
entitlement Apple grants case by case to captive-network-assistant apps. Scan() throws
WifiNotSupportedException on iOS and Mac Catalyst — check
Capabilities.HasFlag(WifiCapabilities.Scan) and offer a “join by name” field instead.
Scan throttling
Section titled “Scan throttling”Android throttles startScan from API 29 (roughly four scans per two minutes in the foreground) and
NetworkManager refuses a scan requested within about ten seconds of the last one. Both serve cached
results instead of failing, so a rapid second scan returns the previous sweep rather than an error.
Do not poll.
Connecting
Section titled “Connecting”var request = new WifiConnectionRequest("Kitchen"){ Passphrase = "hunter2hunter2", Remember = true, Timeout = TimeSpan.FromSeconds(20)};
try{ var joined = await wifi.Connect(request, ct);}catch (WifiConnectionException ex){ // wrong passphrase, out of range, user declined the prompt, or DHCP never answered}Connect returns only once an address has been assigned. Association completes well before DHCP
does, and a WifiNetworkInfo with no IP on it is not much use to the caller.
What each platform does with the request
Section titled “What each platform does with the request”| Field | Android | iOS / Catalyst | macOS | Windows | Linux |
|---|---|---|---|---|---|
Ssid, Passphrase |
✅ | ✅ | ✅ | ✅ | ✅ |
Security |
✅ (picks WPA2 vs WPA3 key mgmt) | WEP flag only | read from beacon | read from beacon | ✅ |
Bssid |
✅ | ignored | ✅ | ✅ | ✅ |
IsHidden |
✅ | ✅ | ✅ | ✅ | ✅ |
Remember |
✅ API 30+ (adds a suggestion) | ✅ (JoinOnce inverted) |
✅ | ✅ | ✅ |
Leave Security as Unknown unless the network is hidden. The platform reads the scheme off the
beacon; a hidden network has no beacon to read, so it has to be told.
Remember saves the network so it can be rejoined later, and is what puts it in
GetKnownNetworks(). Windows, macOS and Linux write an ordinary profile;
iOS keeps the hotspot configuration; Android 11+ registers a WifiNetworkSuggestion next to the
join, because the specifier-based join itself is never persisted.
Disconnecting
Section titled “Disconnecting”await wifi.Disconnect(ct);What this means varies more than the name suggests:
- Android 10+ and iOS drop the network your app asked for. The OS is then free to rejoin one the user had already saved, so the device may not end up offline at all.
- Windows, macOS and Linux disassociate the adapter outright.
The current network
Section titled “The current network”var current = await wifi.GetCurrentNetwork(ct);if (current != null){ Console.WriteLine(current.Ssid); Console.WriteLine(current.IPv4Address); Console.WriteLine(current.Gateway); Console.WriteLine(String.Join(", ", current.DnsAddresses));}WifiNetworkInfo carries Ssid, Bssid, Security, SignalStrengthDbm,
SignalStrengthPercent, FrequencyMhz, Band, Channel, InterfaceName, IpAddresses,
DnsAddresses, Gateway, SubnetMask, and the IPv4Address / IPv6Address shortcuts.
GetCurrentNetwork reads live off the OS on every call, so it is always current but is not free —
hold the result rather than re-reading it in a loop.
Why this is asynchronous
Section titled “Why this is asynchronous”Both phone platforms removed the synchronous answer, and both fail silently when you ask the old way: the call succeeds and the SSID is simply missing.
- iOS 14 deprecated
CNCopyCurrentNetworkInfo, which since that release returns nothing unless your own app configured the network being asked about. Its replacement,NEHotspotNetwork.fetchCurrent, is asynchronous only. It is also richer — iOS now reportsSecurityandSignalStrengthPercenttoo, which CaptiveNetwork never did. - Android 12 (API 31) began redacting the SSID and BSSID out of every pull-style read —
getConnectionInfo, and theWifiInfohanging offgetNetworkCapabilitiesalike — no matter what permissions the app holds. The only source that still discloses them is aNetworkCallbackregistered withFLAG_INCLUDE_LOCATION_INFO, which is push-based.
Shiny registers that callback on Android and calls fetchCurrent on iOS, so a Task is what the
API can honestly return. Everywhere else — macOS, Windows, Linux, plain .NET — the read is
synchronous underneath and the Task completes immediately.
Watching for changes
Section titled “Watching for changes”public sealed class NetworkWatcher(IWifiManager wifi) : IDisposable{ public void Start() => wifi.Changed += this.OnChanged; public void Dispose() => wifi.Changed -= this.OnChanged;
void OnChanged(object? sender, WifiNetworkInfo? network) => this.status = network == null ? "Offline" : $"{network.Ssid} ({network.SignalStrengthPercent}%)";}Changed fires with the new network, or null when the device drops off Wi-Fi entirely.
- Subscribing delivers the current network once, before any change happens, so a fresh
subscriber does not sit blind until the network next moves. You do not need a separate
GetCurrentNetworkcall to seed the handler. - The handler already has the network. It arrives in the event argument, fully populated — a
handler that turns round and calls
GetCurrentNetworkis paying for the read twice. - It is de-duplicated. The native watchers behind it — Android’s
NetworkCallback, Apple’sNWPathMonitor, NetworkManager’sPropertiesChanged— all fire several times for one real change. Only genuine differences are raised. WifiNetworkInfocompares its address lists by value, not by reference, so diffing snapshots yourself works too. The record’s synthesized equality would have compared the arrays by reference and made every poll look like a change.- Unsubscribe. The native watcher is created on the first subscription and torn down on the last, so a leaked handler keeps a radio callback alive for the life of the process.
Powering the radio
Section titled “Powering the radio”if (wifi.Capabilities.HasFlag(WifiCapabilities.RadioToggle)) await wifi.SetRadioEnabled(true, ct);
var isOn = await wifi.GetRadioEnabled(ct);Android revoked setWifiEnabled for third-party apps in API 29 — the capability flag is only set
below that, and above it you should send the user to Settings.Panel.ACTION_WIFI. iOS never allowed
it and does not report the state either. Windows, macOS and Linux support both.
Known networks
Section titled “Known networks”Networks the device has already saved are covered on their own page — Known Networks — including the important caveat that iOS and Android only ever disclose the entries your own app created.
Errors
Section titled “Errors”| Exception | Means | Recoverable |
|---|---|---|
WifiNotSupportedException |
The OS has no API for this. The message names the limit. | No — branch on Capabilities |
WifiPermissionException |
A permission, entitlement or manifest entry is missing. The message names it. | Yes |
WifiConnectionException |
The join failed or timed out | Yes |
WifiException |
The base type for all of the above | Depends |


