The only way I've found to do this in .NET Core is to manually send and receive UDP messages.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ReporterAPI.Utilities.ExtensionMethods;
using Serilog;
public class SqlServerScanner : ISqlServerScanner
{
private readonly ConcurrentDictionary<string, SqlServerInstance> serverInstances = new ConcurrentDictionary<string, SqlServerInstance>();
private DateTime lastScanAttemptTime = DateTime.MinValue;
private readonly TimeSpan cacheValidTimeSpan = TimeSpan.FromSeconds(15);
private readonly TimeSpan responseTimeout = TimeSpan.FromSeconds(2);
private readonly List<UdpClient> listenSockets = new List<UdpClient>();
private readonly int SqlBrowserPort = 1434; // Port SQL Server Browser service listens on.
private const string ServerName = "ServerName";
public IEnumerable<SqlServerInstance> GetList()
{
ScanServers();
return serverInstances.Values;
}
private void ScanServers()
{
lock (serverInstances)
{
if ((DateTime.Now - lastScanAttemptTime) < cacheValidTimeSpan)
{
Log.Debug("Using cached SQL Server instance list");
return;
}
lastScanAttemptTime = DateTime.Now;
serverInstances.Clear();
try
{
var networksInterfaces = Dns.GetHostEntry(Dns.GetHostName()).AddressList;
foreach (var networkInterface in networksInterfaces.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork))
{
Log.Debug("Setting up an SQL Browser listen socket for {address}", networkInterface);
var socket = new UdpClient { ExclusiveAddressUse = false };
socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.Client.Bind(new IPEndPoint(networkInterface, 0));
socket.BeginReceive(new AsyncCallback(ResponseCallback), socket);
listenSockets.Add(socket);
Log.Debug("Sending message to all SQL Browser instances from {address}", networkInterface);
using var broadcastSocket = new UdpClient { ExclusiveAddressUse = false };
broadcastSocket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
broadcastSocket.Client.Bind(new IPEndPoint(networkInterface, ((IPEndPoint)socket.Client.LocalEndPoint).Port));
var bytes = new byte[] { 0x02, 0x00, 0x00 };
broadcastSocket.Send(bytes, bytes.Length, new IPEndPoint(IPAddress.Broadcast, SqlBrowserPort));
}
Thread.Sleep(responseTimeout);
foreach (var socket in listenSockets)
{
socket.Close();
socket.Dispose();
}
listenSockets.Clear();
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to initiate SQL Server browser scan");
throw;
}
}
}
private void ResponseCallback(IAsyncResult asyncResult)
{
try
{
var socket = asyncResult.AsyncState as UdpClient;
var localEndpoint = socket.Client.LocalEndPoint as IPEndPoint;
var bytes = socket.EndReceive(asyncResult, ref localEndpoint);
socket.BeginReceive(new AsyncCallback(ResponseCallback), socket);
if (bytes.Length == 0)
{
Log.Warning("Received nothing from SQL Server browser");
return;
}
var response = System.Text.Encoding.UTF8.GetString(bytes);
Log.Debug("Found SQL Server instance(s): {data}", response);
foreach (var instance in ParseInstancesString(response))
{
serverInstances.TryAdd(instance.FullName(), instance);
}
}
catch (Exception ex) when (ex is NullReferenceException || ex is ObjectDisposedException)
{
Log.Debug("SQL Browser response received after preset timeout {timeout}", responseTimeout);
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to process SQL Browser response");
}
}
/// <summary>
/// Parses the response string into <see cref="SqlServerInstance"/> objects.
/// A single server may have multiple named instances
/// </summary>
/// <param name="serverInstances">The raw string received from the Browser service</param>
/// <returns></returns>
static private IEnumerable<SqlServerInstance> ParseInstancesString(string serverInstances)
{
if (!serverInstances.EndsWith(";;")
{
Log.Debug("Instances string unexpectedly terminates");
yield break;
}
// Remove cruft from instances string.
var firstRecord = serverInstances.IndexOf(ServerName);
serverInstances = serverInstances[firstRecord..^2];
foreach (var instance in serverInstances.Split(";;"))
{
var instanceData = instance.Split(";");
yield return new SqlServerInstance
{
ServerName = instanceData[1],
InstanceName = instanceData[3],
IsClustered = instanceData[5].Equals("Yes"),
Version = instanceData[7]
};
}
}
}
This seems faster than the old SMO EnumerateServers method, probably because it doesn't test each instance for connectivity, which I kind of like. I want to know what servers are out there, even if I have to fix something before being able to actually connect.
This function sends from a randomly selected UDP port, so be aware of the need for a firewall rule to allow traffic for the executable, but you can limit to remote port 1434.