SQL Server SqlException while initializing database, but only on Azure and only with CF Migrations

6bc51xsx  于 2023-06-28  发布在  其他
关注(0)|答案(1)|浏览(87)

I'm getting a SqlException when running initialization against an Azure SQL Server database: The server was not found or was not accessible .

Here's my Context code, in relevant part:

Private Sub New(Connection As DbConnection)
  MyBase.New(Connection, True)

  Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
  Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))

  Me.Database.Initialize(False)
End Sub

Public Shared Function Create() As Context
  Return New Context(DbConnection)
End Function

Here's the full exception stack trace:

1. System.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server) ---> System.ComponentModel.Win32Exception (0x80004005): Access is denied
 2.    at System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, Object providerInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString userConnectionOptions, SessionData reconnectSessionData, DbConnectionPool pool, String accessToken, Boolean applyTransientFaultHandling, SqlAuthenticationProviderManager sqlAuthProviderManager)
 3.    at System.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions)
 4.    at System.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions)
 5.    at System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
 6.    at System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
 7.    at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
 8.    at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection)
 9.    at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
10.    at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
11.    at System.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
12.    at System.Data.SqlClient.SqlConnection.TryOpenInner(TaskCompletionSource`1 retry)
13.    at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
14.    at System.Data.SqlClient.SqlConnection.Open()
15.    at System.Data.Entity.Infrastructure.Interception.DbConnectionDispatcher.<>c.<Open>b__13_0(DbConnection t, DbConnectionInterceptionContext c)
16.    at System.Data.Entity.Infrastructure.Interception.InternalDispatcher`1.Dispatch[TTarget,TInterceptionContext](TTarget target, Action`2 operation, TInterceptionContext interceptionContext, Action`3 executing, Action`3 executed)
17.    at System.Data.Entity.Infrastructure.Interception.DbConnectionDispatcher.Open(DbConnection connection, DbInterceptionContext interceptionContext)
18.    at System.Data.Entity.SqlServer.SqlProviderServices.<>c__DisplayClass60_0.<UsingConnection>b__0()
19.    at System.Data.Entity.Infrastructure.DbExecutionStrategy.<>c__DisplayClass17_0.<Execute>b__0()
20.    at System.Data.Entity.Infrastructure.DbExecutionStrategy.Execute[TResult](Func`1 operation)
21.    at System.Data.Entity.Infrastructure.DbExecutionStrategy.Execute(Action operation)
22.    at System.Data.Entity.SqlServer.SqlProviderServices.UsingConnection(DbConnection sqlConnection, Action`1 act)
23.    at System.Data.Entity.SqlServer.SqlProviderServices.UsingMasterConnection(DbConnection sqlConnection, Action`1 act)
24.    at System.Data.Entity.SqlServer.SqlProviderServices.CreateDatabaseFromScript(Nullable`1 commandTimeout, DbConnection sqlConnection, String createDatabaseScript)
25.    at System.Data.Entity.SqlServer.SqlProviderServices.DbCreateDatabase(DbConnection connection, Nullable`1 commandTimeout, StoreItemCollection storeItemCollection)
26.    at System.Data.Entity.Core.Common.DbProviderServices.CreateDatabase(DbConnection connection, Nullable`1 commandTimeout, StoreItemCollection storeItemCollection)
27.    at System.Data.Entity.Core.Objects.ObjectContext.CreateDatabase()
28.    at System.Data.Entity.Migrations.Utilities.DatabaseCreator.Create(DbConnection connection)
29.    at System.Data.Entity.Migrations.DbMigrator.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
30.    at System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration)
31.    at System.Data.Entity.MigrateDatabaseToLatestVersion`2.InitializeDatabase(TContext context)
32.    at System.Data.Entity.Internal.InternalContext.<>c__DisplayClass66_0`1.<CreateInitializationAction>b__0()
33.    at System.Data.Entity.Internal.InternalContext.PerformInitializationAction(Action action)
34.    at System.Data.Entity.Internal.InternalContext.PerformDatabaseInitialization()
35.    at System.Data.Entity.Internal.LazyInternalContext.<>c.<InitializeDatabase>b__58_0(InternalContext c)
36.    at System.Data.Entity.Internal.RetryAction`1.PerformAction(TInput input)
37.    at System.Data.Entity.Internal.LazyInternalContext.InitializeDatabaseAction(Action`1 action)
38.    at System.Data.Entity.Internal.LazyInternalContext.InitializeDatabase()
39.    at System.Data.Entity.Internal.InternalContext.Initialize()
40.    at Website.Db.Context..ctor(DbConnection Connection) in D:\Dev\Application\__Legacy\Website\Db\Context.vb:line 24
41. ClientConnectionId:00000000-0000-0000-0000-000000000000
42. Error Number:5,State:0,Class:20

Now, when I insert some test code prior to the initialization attempt, e.g.:

Public Shared Function Create() As Context
  Return New Context(DbConnection)
End Function

Private Sub New(Connection As DbConnection)
  MyBase.New(Connection, True)

  Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
  Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))

  Dim sConnectionString As String = Connection.ConnectionString

  HttpContext.Current.Response.Write($"Connection string: {sConnectionString}")
  HttpContext.Current.Response.Write("<br />")
  HttpContext.Current.Response.Write($"Can connect: {Db.Connection.CanConnect(sConnectionString)}")
  HttpContext.Current.Response.End()

  Try
    Me.Database.Initialize(False)

  Catch ex As Exception
    HttpContext.Current.Response.Write(ex.ToString)
    HttpContext.Current.Response.End()

  End Try
End Sub

... my string is correct and the connection sails through just fine. It's only when I try to connect via my context that it fails (i.e. Me.Database.Initialize(False) ).

Here's my CanConnect code:

Public Shared Function CanConnect(ConnectionString As String) As Boolean
  CanConnect = True

  Try
    Utils.ExecuteNonQuery("SELECT * FROM INFORMATION_SCHEMA.TABLES", ConnectionString)

  Catch ex As Exception
    CanConnect = False

  End Try
End Function

Public Shared Function ExecuteNonQuery(CommandText As String, ConnectionString As String) As Integer
  Using oSqlCnn As New SqlConnection(ConnectionString)
    Using oSqlCmd As SqlCommand = oSqlCnn.CreateCommand
      oSqlCmd.CommandType = CommandType.Text
      oSqlCmd.CommandText = CommandText
      oSqlCnn.Open()

      Return oSqlCmd.ExecuteNonQuery
    End Using
  End Using
End Function

So a plain-vanilla ADO.NET connection gets through while an EF Code First connection doesn't—both using the same connection string. Very odd.

Further compounding the mystery is the fact that all of this works great on my dev machine connecting to a locally-installed SQLEXPRESS instance. My context has no problem connecting and applying the migrations here. It only fails on Azure.

Searches reveal nothing, mostly only advice on how to properly configure a migration scenario. I've already done that, as evidenced on my dev machine.

I looked briefly at SqlException 's members, with the thought that I might get the connection string and review it for accuracy, but that's not an option.

Why is this connection attempt failing only under the context, and only on Azure? How can I track it down so I can fix it?

--EDIT--

Here's my connection string (sanitized for security):

Server=tcp:some.database.windows.net,1433;Initial Catalog=somedb;Persist Security Info=False;User ID=username;Password=password;

--EDIT--

There's been some halting progress, but the new information clouds the picture even further.

According to this answer, for which I'm waiting for a confirmation from an Azure SQL engineer, System.Data.SqlClient first attempts a TCP connection (assuming it's instructed to do so via a tcp: prefix in the connection string). If that first attempt fails, the client falls back to Named Pipes.

This would seem to be what's happening in this case, as the stack trace indicates a Named Pipes failure (as helpfully noticed by @AlwaysLearning, in the comments). Of course Named Pipes isn't supported by Azure SQL, so a connection failure would be expected at that point.

So I first thought we were one step closer to a solution, until I encountered this: a query of the server's sys.event_log reveals a nearly unanimous record of successful connections.

This is getting really strange.

--EDIT--

The test on my local dev machine of disabling Named Pipes didn't reveal anything. The connection sailed through just fine on TCP alone.

So that narrows it down a bit. Failure is occurring only via EF6, and only on Azure. (But the connection log shows success.)

This is a real head-scratcher.

--EDIT--

I've been able to confirm that the successful connections in the log are from the ADO.NET connections from my testing (above).

The failed connections don't hit the log, as the server can't be found in order to log them (at least the Named Pipes attempts). This remains the big mystery.

--EDIT--

I opened an issue at the EntityFramework repo and published a repro project:

--EDIT--

The clues keep coming in, but they continue to point in all different directions.

I have another database on the same server whose website uses the exact same Migrations code that I'm using here. That application works just fine, and the connection string is nearly identical.

Confusing. But this problem is at the server level, not at the database level.

Is it possible to intercept the connection string before it hits the database, to verify that it's not been corrupted on its way up the stack?

aurhwmvo

aurhwmvo1#

The problem is solved.

The savvy developer (not me) will note in the Context class from commit 3c20e41 of the repro project that I was building two separate connection strings in two different places. At the time, I was under the mistaken impression that CF Migrations only uses the default constructor for design-time operations, e.g. Add-Migration , Update-Database , etc.

It turns out that this is emphatically not the case. DbMigrator runtime code execution does indeed loop back to call the default constructor (there's a hidden clue: see line 31 in the stack trace above). And the default constructor is where I was getting the design-time connection string. That's why the Azure-based connection attempt was failing—because it couldn't find the server name provided by the design-time string. Naturally.

Now check the repaired code from commit d45888b. There's only one connection string source, and that source only builds the design-time string if Configuration.ConnectionStrings is Nothing . (I've had that happen before, which is why I built the two-string concept—I just didn't go far enough with it.)

And that's why it was working fine on my local dev machine—because I was using the design-time string.

I only discovered all this by sheer accident. I was working out another testing scenario and I got the username and password values mixed up in my design-time string builder. The connection failure report indicated a login failure this time, displaying the username that was attempted. Wait a minute! That's my password! Duh.

The only way (under that version of the code) that the design-time string could have been produced was during a call to the default constructor. Which, according to my previous understanding, didn't occur at runtime.

Live and learn.

Here's the code directly, in case at some point in the distant future I decide to take the repo down. Possible, but unlikely. I'd rather leave it up as a memento of questionable programming practices.

Old Context (broken)

Imports System.Data.Common
Imports System.Data.Entity
Imports System.Data.SqlClient
Imports System.Reflection
Imports DbConnectionTest.Db.Models

Namespace Db
  Public Class Context
    Inherits DbContext

    Public Sub New()
      MyBase.New(Utils.DesignTimeConnectionString) ' <-- Oops
    End Sub

    Private Sub New(Connection As DbConnection)
      MyBase.New(Connection, True)

      Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
      Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))

      Me.Database.Initialize(False)
    End Sub

    Public Shared Function Create() As Context
      Return New Context(New SqlConnection(Utils.RunTimeConnectionString)) ' <-- Oops
    End Function

    Protected Overrides Sub OnModelCreating(Builder As DbModelBuilder)
      Builder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly)
      MyBase.OnModelCreating(Builder)
    End Sub

    Protected Overrides Sub Dispose(Disposing As Boolean)
      MyBase.Dispose(Disposing)
    End Sub

    Public Overridable Property Customers As DbSet(Of Customer)
    Public Overridable Property Invoices As DbSet(Of Invoice)
  End Class
End Namespace

New Utils (now working)

Imports System
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient

Namespace Db
  Friend Class Utils
    Friend Shared ReadOnly Property DbConnectionString() As String
      Get
        If ConfigurationManager.ConnectionStrings Is Nothing Then
          With New SqlConnectionStringBuilder
            .MultipleActiveResultSets = True
            .PersistSecurityInfo = False
            .IntegratedSecurity = False
            .InitialCatalog = DB_NAME
            .DataSource = Environment.MachineName
            .Password = ""
            .UserID = ""

            DbConnectionString = .ConnectionString
          End With
        Else
          DbConnectionString = ConfigurationManager.ConnectionStrings(DB_NAME).ConnectionString
        End If
      End Get
    End Property

    Friend Const DB_NAME As String = "DbConnectionTest"
  End Class
End Namespace

相关问题