Search for all SQL Server instances on network with .NET Core 3.1

Viewed 1303

My application requires a user to select a SQL Server instance on their network to serve as the app's database server. I need to populate a list of all available SQL Server instances from which to make this selection.

The application is developed in .NET Core 3.1. I am unable to simply use SqlDataSourceEnumerator as it is not available in .NET Core.

Is there an alternative available to make this query?

I'd like to avoid importing PowerShell functionality to the application if possible, as it would likely require additional PowerShell modules to be installed on the machine, but will consider it if it's the only way.

1 Answers

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.

Related