Anp.PnpDeviceDiscovery 1.0.1

dotnet add package Anp.PnpDeviceDiscovery --version 1.0.1
                    
NuGet\Install-Package Anp.PnpDeviceDiscovery -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Anp.PnpDeviceDiscovery" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Anp.PnpDeviceDiscovery" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Anp.PnpDeviceDiscovery" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Anp.PnpDeviceDiscovery --version 1.0.1
                    
#r "nuget: Anp.PnpDeviceDiscovery, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Anp.PnpDeviceDiscovery@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Anp.PnpDeviceDiscovery&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Anp.PnpDeviceDiscovery&version=1.0.1
                    
Install as a Cake Tool

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 PnpDeviceEnumerator and watching via PnpDeviceWatcher (events fire on a thread‑pool thread).
  • Device info: Device path (symbolic link), display/friendly name, description, manufacturer, VID/PID (when parseable), COM/LPT port 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: CreateFile or CreateFile2
  • .NET: FileStream, SerialPort etc.
  • 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 SerialPort class 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.
  • DevicePathFilter optionally 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/StopWatching calls
  • 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .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.

Version Downloads Last Updated
1.0.1 102 2/21/2026
1.0.0 118 1/9/2026

- 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.