The SQL Server instance returned an invalid or unsupported protocol version during login negotiation

.NET Core does not support SQL Server versions lower than SQL Server 2005. Or rather, it does not support TDS protocol versions lower than TDS 7.2 (the version introduced by SQL Server 2005). The relevant bit of source:

a.tdsVersion = (UInt32)((((((b[0] << 8) | b[1]) << 8) | b[2]) << 8) | b[3]); // bytes are in motorola order (high byte first)
UInt32 majorMinor = a.tdsVersion & 0xff00ffff;
UInt32 increment = (a.tdsVersion >> 16) & 0xff;

// Server responds:
// 0x07000000 -> Sphinx         // Notice server response format is different for bwd compat
// 0x07010000 -> Shiloh RTM     // Notice server response format is different for bwd compat
// 0x71000001 -> Shiloh SP1
// 0x72xx0002 -> Yukon RTM
// information provided by S. Ashwin

switch (majorMinor)
{
    case TdsEnums.YUKON_MAJOR << 24 | TdsEnums.YUKON_RTM_MINOR:     // Yukon
        if (increment != TdsEnums.YUKON_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isYukon = true;
        break;
    case TdsEnums.KATMAI_MAJOR << 24 | TdsEnums.KATMAI_MINOR:
        if (increment != TdsEnums.KATMAI_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isKatmai = true;
        break;
    case TdsEnums.DENALI_MAJOR << 24 | TdsEnums.DENALI_MINOR:
        if (increment != TdsEnums.DENALI_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isDenali = true;
        break;
    default:
        throw SQL.InvalidTDSVersion();
}

For reference: Sphinx is 7.0, Shiloh is 2000, Yukon is 2005, Katmai is 2008, Denali is 2012 (these are the code names). These names are misleading because the version verified here is the TDS protocol version, not the SQL Server version. SQL Server 2012, 2014 and 2016 all use TDS 7.4 (per this reference), which is why there are no checks beyond Denali.

Here's that same piece of code from the reference source of the .NET Framework:

a.tdsVersion = (UInt32)((((((b[0]<<8)|b[1])<<8)|b[2])<<8)|b[3]); // bytes are in motorola order (high byte first)
UInt32 majorMinor = a.tdsVersion & 0xff00ffff;
UInt32 increment  = (a.tdsVersion >> 16) & 0xff;

// Server responds:
// 0x07000000 -> Sphinx         // Notice server response format is different for bwd compat
// 0x07010000 -> Shiloh RTM     // Notice server response format is different for bwd compat
// 0x71000001 -> Shiloh SP1
// 0x72xx0002 -> Yukon RTM
// information provided by S. Ashwin

switch (majorMinor) {
    case TdsEnums.SPHINXORSHILOH_MAJOR<<24|TdsEnums.DEFAULT_MINOR:    // Sphinx & Shiloh RTM
        // note that sphinx and shiloh_rtm can only be distinguished by the increment
        switch (increment) {
            case TdsEnums.SHILOH_INCREMENT:
                _isShiloh = true;
                break;
            case TdsEnums.SPHINX_INCREMENT:
                // no flag will be set
                break;
            default:
                throw SQL.InvalidTDSVersion();
        }
        break;
    case TdsEnums.SHILOHSP1_MAJOR<<24|TdsEnums.SHILOHSP1_MINOR: // Shiloh SP1
        if (increment != TdsEnums.SHILOHSP1_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isShilohSP1 = true;
        break;
    case TdsEnums.YUKON_MAJOR<<24|TdsEnums.YUKON_RTM_MINOR:     // Yukon
        if (increment != TdsEnums.YUKON_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isYukon = true;
        break;
    case TdsEnums.KATMAI_MAJOR<<24|TdsEnums.KATMAI_MINOR:
        if (increment != TdsEnums.KATMAI_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isKatmai = true;
        break;
    case TdsEnums.DENALI_MAJOR << 24|TdsEnums.DENALI_MINOR:
        if (increment != TdsEnums.DENALI_INCREMENT) { throw SQL.InvalidTDSVersion(); }
        _isDenali = true;
        break;
    default:
        throw SQL.InvalidTDSVersion();
}

Support for TDS 7.0 and 7.1 is clearly visible.

I couldn't find information online on the decision not to support anything lower than SQL Server 2005 in .NET Core and the lack of support is already present in the earliest commit (from September 2015). Given that SQL Server 2000 has been out of extended support since 2013, this is not unreasonable.

In short: either stick to using the full .NET Framework (which has not (yet) dropped support for SQL Server 2000), or upgrade your server (highly advisable). Conceivably, you could also fork the code to backport SQL Server 2000 support to .NET Core, but this is almost certainly not worth the effort.


This is an old answer, and it was originally for dotnetcore 1.1, but now with dotnetcore 2.2 released, OdbcConnection is supported, which will let you use any old Odbc drivers. So something like this will now let you connect to a SQL Server 2000 database:

            using (var conn =
                new OdbcConnection("Driver={SQL Server};Server=<YOUR_SERVER>;Database=<YOUR_DB>;Trusted_Connection=True;"))
            {
                conn.Open();
                var cmd = new OdbcCommand("SELECT * FROM SOMETABLE", conn);
                var reader = cmd.ExecuteReader();
                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        var values = new Object[reader.FieldCount];
                        var fieldCount = reader.GetValues(values);

                        Console.WriteLine("Found {0} columns.",
                            fieldCount);
                        for (int i = 0; i < fieldCount; i++)
                            Console.WriteLine(values[i]);

                        Console.WriteLine();
                    }
                }
            }

There are other drivers that will work as well, not just {SQL Server}. Check out: https://www.connectionstrings.com/sql-server-2000/ for other ODBC connection strings (need to scroll down to the ODBC sections).