Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

Network Discovery | Getting Started

Find things on the local network and advertise your own, across the three protocols consumer and commercial hardware actually uses: mDNS/DNS-SD (Bonjour/Zeroconf), SSDP/UPnP, and WS-Discovery. Discover printers, casting targets, IoT devices, routers, media servers, ONVIF cameras, and other instances of your own app. Built on Shiny.Core, so it runs in any Shiny host — MAUI, console, service, or server.

GitHub GitHub stars for shinyorg/shiny
Downloads NuGet downloads for Shiny.Net.Discovery
Frameworks
.NET MAUI
.NET
Operating Systems
Android
iOS
Windows
Linux
macOS

Register only what you need — they are independent, and a device that speaks one usually does not speak the others.

Looking for Protocol Register Docs
Apple devices, AirPlay, IPP printers, peer apps, your own services mDNS/DNS-SD AddMdns() Browsing · Publishing
Routers, UPnP/DLNA media servers, Sonos, Roku, smart TVs SSDP/UPnP AddSsdp() SSDP & UPnP
ONVIF IP cameras, WSD printers and scanners, Windows machines WS-Discovery AddWsDiscovery() WS-Discovery & ONVIF
  • Browse — a live IAsyncEnumerable<> stream of things appearing and disappearing, or a one-shot scan, for all three protocols.
  • Resolve — turn an mDNS instance name into a host, port, IPv4/IPv6 addresses, and TXT records.
  • Publish — advertise your own mDNS service, UPnP root device, or WS-Discovery target.
  • UPnP device descriptions — fetch and parse friendly name, model, icons, and the service list.
  • No iOS multicast entitlement for mDNS — Apple platforms go through Bonjour (NSNetService), not raw multicast sockets. SSDP and WS-Discovery do need it; see Platform Setup.
  • No Android multicast lock to manage — mDNS goes through NsdManager, and the lock SSDP/WS-Discovery need is acquired for you.
  • Actionable permission errors — a missing entitlement or manifest entry throws DiscoveryPermissionException naming what to add, instead of silently finding nothing.
  • Dependency-free managed implementations, no Avahi and no third-party stack.
  • AOT & trimmer compatible.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-discovery Skill
Shiny.Net.DiscoveryNuGet package Shiny.Net.Discovery

Install the package:

Terminal window
dotnet add package Shiny.Net.Discovery

Register the manager:

using Shiny;
builder.Services.AddMdns(); // IMdnsManager
builder.Services.AddSsdp(); // ISsdpManager
builder.Services.AddWsDiscovery(); // IWsDiscoveryManager

Then inject the manager you need.

public class PrinterFinder(IMdnsManager mdns)
{
public async Task<IReadOnlyList<MdnsService>> FindPrinters(CancellationToken ct)
=> await mdns.BrowseOnce("_ipp._tcp", TimeSpan.FromSeconds(5), ct);
}

BrowseOnce collects for a fixed window and returns everything still present when it closes — ideal for a “scan” button. For a list that updates as devices come and go, use the streaming API:

await foreach (var result in mdns.Browse("_ipp._tcp", ct))
{
if (result.Status == MdnsBrowseStatus.Found)
this.printers[result.Service.FullName] = result.Service;
else
this.printers.Remove(result.Service.FullName);
}

Browse never completes on its own — it runs until the CancellationToken is cancelled.

var service = found.First();
Console.WriteLine(service.InstanceName); // "Kitchen Printer"
Console.WriteLine(service.HostName); // "kitchen-printer.local"
Console.WriteLine(service.Port); // 631
Console.WriteLine(service.GetTxt("rp")); // TXT record lookup
if (service.IsResolved)
await socket.ConnectAsync(service.GetEndPoint()!);
await using var publication = await mdns.Publish(
new MdnsServiceRegistration("Allan's Laptop", "_myapp._tcp", 8080)
{
TxtRecords = new Dictionary<string, string> { ["version"] = "2" }
},
ct
);
// the responder renames on conflict, so read the live name back
logger.LogInformation("advertising as {Name}", publication.InstanceName);

Publishing only advertises the port — you are still responsible for having a listener bound to it. Disposing the publication sends a goodbye packet and stops advertising.