Anp.PnpDeviceDiscovery
1.0.1
dotnet add package Anp.PnpDeviceDiscovery --version 1.0.1
NuGet\Install-Package Anp.PnpDeviceDiscovery -Version 1.0.1
<PackageReference Include="Anp.PnpDeviceDiscovery" Version="1.0.1" />
<PackageVersion Include="Anp.PnpDeviceDiscovery" Version="1.0.1" />
<PackageReference Include="Anp.PnpDeviceDiscovery" />
paket add Anp.PnpDeviceDiscovery --version 1.0.1
#r "nuget: Anp.PnpDeviceDiscovery, 1.0.1"
#:package Anp.PnpDeviceDiscovery@1.0.1
#addin nuget:?package=Anp.PnpDeviceDiscovery&version=1.0.1
#tool nuget:?package=Anp.PnpDeviceDiscovery&version=1.0.1
PnpDeviceDiscovery
Windows Plug‑and‑Play (PnP) device discovery and monitoring library built on the modern Configuration Manager (CfgMgr32) API. Enumerates device interfaces and raises arrival/removal events without the use of SetupDi API, WMI, registry traversal, or hidden windows.
OS support: Windows 8+.
Features
- Self‑contained and lightweight; no external dependencies.
- .NET Framework 4.8, .NET Standard 2.0, .NET 8.0, C# 7.3.
- CfgMgr32 path only (Unified Device Property Model): no SetupDi, WMI, registry crawling, or hidden windows.
- Enumeration via
PnpDeviceEnumeratorand watching viaPnpDeviceWatcher(events fire on a thread‑pool thread). - Device info: Device path (symbolic link), display/friendly name, description, manufacturer,
VID/PID(when parseable),COM/LPTport label (when applicable), presence, power state, parent info, and more.
This library focuses on device discovery only. For device communication, use the device path (symbolic link) with:
- Windows API:
CreateFileorCreateFile2- .NET:
FileStream,SerialPortetc.- Third-party libraries: Pass as device identifier
The COM port name, paired with other Device info, permits communication with USB CDC (USB Serial) devices via the .NET
SerialPortclass without the need for a COM port selection dialog.
Install NuGet
Quick start
Enumerate devices (known interface class GUID)
using System;
using Anp.PnpDeviceDiscovery;
class Program
{
static void Main()
{
// USB devices
var interfaceClass = new Guid("{A5DCBF10-6530-11D2-901F-00C04FB951ED}");
foreach (var d in PnpDeviceEnumerator.EnumerateDevices(
interfaceClass, DevicePresenceFilter.Present, devicePathFilter: "VID_1234"))
{
Console.WriteLine(d.DisplayName ?? d.Description);
Console.WriteLine($" Path: {d.DevicePath}");
Console.WriteLine($" VID: {d.VendorId?.ToString("X4")} PID: {d.ProductId?.ToString("X4")}");
Console.WriteLine($" Port: {d.PortName}");
Console.WriteLine($" Present: {d.IsPresent} Power: {d.PowerState}");
Console.WriteLine();
}
}
}
Discover the interface class GUID (when unknown)
Option A — Microsoft Learn
Option B — watch all classes temporarily:
using System;
using Anp.PnpDeviceDiscovery;
class Program
{
static void Main()
{
using (var watcher = new PnpDeviceWatcher())
{
watcher.DevicePathFilter = "VID_1234"; // optional substring filter
watcher.DeviceArrived += (s, info) =>
{
Console.WriteLine($"ARRIVED: {info.DevicePath}");
Console.WriteLine($"Class GUID: {info.InterfaceClassGuid}");
};
watcher.DeviceRemoved += (s, info) =>
Console.WriteLine($"REMOVED: {info.DevicePath}");
watcher.StartWatching(Guid.Empty); // Guid.Empty → all classes
Console.WriteLine("Watching all classes. Press <Enter> to stop.");
Console.ReadLine();
}
}
}
Option C — enumerate all, then filter:
foreach (var d in PnpDeviceEnumerator.EnumerateDevices()) // default → all classes
{
if (!string.IsNullOrEmpty(d.DevicePath) &&
d.DevicePath.IndexOf("VID_1234", StringComparison.OrdinalIgnoreCase) >= 0)
{
Console.WriteLine($"Found class GUID: {d.InterfaceClassGuid}");
break;
}
}
Watch a targeted class for arrival/removal
using System;
using Anp.PnpDeviceDiscovery;
class Program
{
static void Main()
{
// USB devices
var interfaceClass = new Guid("{A5DCBF10-6530-11D2-901F-00C04FB951ED}");
using (var watcher = new PnpDeviceWatcher())
{
watcher.DevicePathFilter = "VID_1234"; // optional
watcher.DeviceArrived += (s, info) =>
{
Console.WriteLine($"ARRIVED: {info.DisplayName ?? info.Description}");
Console.WriteLine($" Path: {info.DevicePath}");
Console.WriteLine($" Port: {info.PortName} Present: {info.IsPresent}");
};
watcher.DeviceRemoved += (s, info) =>
Console.WriteLine($"REMOVED: {info.DisplayName ?? info.Description}");
watcher.StartWatching(interfaceClass);
Console.WriteLine("Watching targeted class. Press <Enter> to exit.");
Console.ReadLine();
}
}
}
Notes:
- Events fire on a thread‑pool thread; UI frameworks should marshal accordingly.
DevicePathFilteroptionally reduces noise by applying a case‑insensitive substring filter.- Lifetime: The watcher keeps itself alive automatically while a native registration is active.
Callers still should
Dispose()the instance when done to release system resources promptly.
Thread safety
- PnpDeviceEnumerator: All methods are thread-safe for concurrent enumeration
- PnpDeviceWatcher: Thread-safe for concurrent
StartWatching/StopWatchingcalls - PnpDeviceInfo: Immutable after creation, safe to share across threads
- Event Handlers: Called on thread pool threads - marshal to UI thread if needed
Object model
public static class PnpDeviceEnumerator
{
// Enumerate by interface class; optional presence and substring filters.
public static IEnumerable<PnpDeviceInfo> PnpDeviceEnumerator.EnumerateDevices(
Guid interfaceClassGuid = default,
DevicePresenceFilter presenceFilter = DevicePresenceFilter.Present,
string devicePathFilter = default); // may throw Win32Exception
// Enumerate all interface class GUIDs registered on the system.
public static IEnumerable<Guid> EnumerateInterfaceClasses();
}
public sealed class PnpDeviceWatcher : IDisposable
{
public string DevicePathFilter { get; set; }
public event EventHandler<PnpDeviceInfo> DeviceArrived;
public event EventHandler<PnpDeviceInfo> DeviceRemoved;
public void StartWatching(Guid interfaceClassGuid = default); // Guid.Empty → all
public void StopWatching(Guid interfaceClassGuid = default); // Guid.Empty → stop all
}
PnpDeviceInfo (selected properties): DisplayName, FriendlyName, Description, Manufacturer, VendorId, ProductId, PortName, SetupClassName, EnumeratorName, ServiceName, Location, BusAddress, DevicePath, InstanceId, InterfaceClassGuid, PowerState, ParentInstanceId, ParentDisplayName.
Diagnostics
Subscribe to PnpDiag.Error to receive error notifications; errors also emit to the Debug stream in DEBUG builds.
Anp.PnpDeviceDiscovery.Diagnostics.PnpDiag.Error += (s, e) =>
System.Diagnostics.Debug.WriteLine($"[{e.Timestamp}] {e.Source}: {e.Message}");
Build & support
- Targets: .NET Framework 4.8, .NET Standard 2.0, .NET 8.0
- Language: C# 7.3
- Interop: CfgMgr32 P/Invoke (Unified Device Property Model)
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net8.0-windows8.0 is compatible. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.8
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
-
net8.0-windows8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
- PnpDeviceWatcher sealed; GC lifetime hazard fixed via SafeHandle keep-alive — both the watcher instance and its callback delegate stay alive for the duration of each active native registration, preventing silent event-delivery stoppage or unreachable-delegate crashes.
- StopWatching and StartWatching no longer hold the lock during CM_Unregister_Notification (eliminates latent deadlock risk when in-flight callbacks try to acquire the same lock).
- Device path filter in both PnpDeviceEnumerator and PnpDeviceWatcher broadened from IsNullOrEmpty to IsNullOrWhiteSpace.
- PnpDiag.Error static event: sender corrected to null (convention); doc warning added for memory-leak risk.
- SafeCmNotificationHandle.ReleaseHandle uses Debug.WriteLine instead of PnpDiag (logger may already be torn down during finalization).
- StringExtensions.SubstringOrNull: removed unreachable guards; overflow-safe bounds check.
- Minor doc corrections: DevNode capitalisation, stale XML doc quotes, broken see-cref, DevicePathParser VID&PID example.