0

I have read the numerous posts on why you should give the using statement preference over manually doing .Open() then .Close() and finally .Dispose().

When I initially wrote my code, I had something like this:

private static void doIt(string strConnectionString, string strUsername)
{
    SqlConnection conn = new SqlConnection(strConnectionString);
    try
    {
        conn.Open();
        string strSqlCommandText = $"CREATE USER {strUsername} for LOGIN {strUsername} WITH DEFAULT SCHEMA = [dbo];";
        SqlCommand sqlCommand = new SqlCommand(strSqlCommandText, conn);
        var sqlNonReader = sqlCommand.ExecuteNonQuery();
        if (sqlNonReader == -1) Utility.Notify($"User Added: {strUsername}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
    finally
    {
        conn.Close();
        conn.Dispose();
    }
}

and this works... no problem. but only ONCE.

so, if I do something like this:

private static void doItLots(string strConnectionString, string strUsername)
{
    for(int i=0; i<10; i++)
    {
        doIt(strConnectionString, $"{strUsername}_{i}");
    }
}

it works the FIRST time when i=0, but any subsequent iterations fail with Cannot open database "myDbName" requested by the login. The login failed.

However, if I go back and comment out the conn.Dispose(); line, then it works fine on all iterations.

The problem is simply that if I want to do the .Dispose() part outside of the method, then I am forced to pass a SqlConnection object instead of simply passing the credentials, potentially making my code a bit less portable and then I need to keep the connection around longer as well. I was always under the impression that you want to open and close connections quickly but clearly I'm misunderstanding the way the .Dispose() command works.

As I stated at the outset, I also tried doing this with using like this...

private static void doIt(string strConnectionString, string strUsername)
{
    using (SqlConnection conn = new SqlConnection(strConnectionString))
    {
        try
        {
            conn.Open();
            string strSqlCommandText = $"CREATE USER {strUsername} for LOGIN {strUsername} WITH DEFAULT SCHEMA = [dbo];";
            SqlCommand sqlCommand = new SqlCommand(strSqlCommandText, conn);
            var sqlNonReader = sqlCommand.ExecuteNonQuery();
            if (sqlNonReader == -1) Utility.Notify($"User Added: {strUsername}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        finally
        {
            conn.Close();
        }
    }
}

and this does the exact same thing as the initial code with .Dispose() called manually.

Any help here would be greatly appreciated. I'd love to convert to the using statements but having trouble figuring out how to write reusable methods that way...

UPDATE:

I have narrowed it down a bit. The issue is NOT the iterations or making the calls over-and-over again. But I am still getting an access error. Here is the code:

        string strConnectionString = $@"Data Source={StrSqlServerDataSource};Initial Catalog={StrDatabaseName};User id={StrSqlServerMasterUser};Password={StrSqlServerMasterPassword}";

        using (SqlConnection connUserDb = new SqlConnection(strConnectionString))
        {
            try
            {
                Utility.Notify($"Connection State: {connUserDb.State.ToString()}"); // Responds as 'Closed'
                connUserDb.Open(); // <-- throws error
                Utility.Notify($"Connection State: {connUserDb.State.ToString()}");

                Utility.Notify($"MSSQL Connection Open... Adding User '{strUsername}' to Database: '{strDatabaseName}'");

                string sqlCommandText =
                    //$@"USE {StrDatabaseName}; " +
                    $@"CREATE USER [{strUsername}] FOR LOGIN [{strUsername}] WITH DEFAULT_SCHEMA = [dbo]; " +
                    $@"ALTER ROLE [db_datareader] ADD MEMBER [{strUsername}]; " +
                    $@"ALTER ROLE [db_datawriter] ADD MEMBER [{strUsername}]; " +
                    $@"ALTER ROLE [db_ddladmin] ADD MEMBER [{strUsername}]; ";
                using (SqlCommand sqlCommand = new SqlCommand(sqlCommandText, connUserDb))
                {
                    var sqlNonReader = sqlCommand.ExecuteNonQuery();
                    if (sqlNonReader == -1) Utility.Notify($"User Added: {strUsername} ({sqlNonReader})");
                }

                result = true;
            }
            catch (Exception ex)
            {
                Utility.Notify($"Creating User and Updating Roles Failed: {ex.Message}", Priority.High);
            }
            finally
            {
                connUserDb.Close();
                Utility.Notify($"MSSQL Connection Closed");
            }
        }
        return result;
    }

The error I am getting here is: Cannot open database requested by the login. The login failed.

One clue I have is that prior to this, I was running this same code with two changes:

1) uncommented the USE statement in the sqlCommandText 2) connected to the Master database instead

When I did that, it didn't work either, and instead I got this error: The server principal is not able to access the database under the current security context.

If I go into SSMS and review the MasterUser they are listed as db_owner and I can perform any activities I want, including running the command included in the code above.

I rewrote all the code to make use of a single connection per the recommendations here. After running into the "server principal" error, I added one more connection to attempt to directly connect to this database rather than the master.

UPDATE 2:

Here is another plot twist...

This is working from my local computer fine (now). But, not (always) working when run from an Azure Webjob that targets an Amazon Web Services (AWS) Relational Database Server (RDS) running MSSQL.

I will have to audit the git commits tomorrow, but as of 5p today, it was working on BOTH local and Azure. After the last update, I was able to test local and get it to work, but when run on Azure Webjob it failed as outlined above.

gotmike
  • 1,515
  • 4
  • 20
  • 44
  • 1
    Before addressing the error, you need to quote your user name. `string quotedUserName = "[" + strUserName.Replace("]", "]]") + "]";` – madreflection Sep 19 '19 at 16:58
  • Use the last version, but get rid of the `finally`. One of the core virtues of `using` is that it will close the connection when it passes out of the `using` block scope, regardless of whether it does that on its feet or in a coffin. And use SqlParameters, not string interpolation, for parameters. What you're doing is a sql injection vulnerability. You can have the connection string in a static property or in config or something. – 15ee8f99-57ff-4f92-890c-b56153 Sep 19 '19 at 16:58
  • BTW, your code isn't more portable because you're passing connection strings around, it's the same thing, as the string is tighly coupled to the DB engine. – Alejandro Sep 19 '19 at 17:00
  • @madreflection -- good call, will do. – gotmike Sep 19 '19 at 17:05
  • `CREATE LOGIN` won't accept a parameter for the login name, which is why I suggested a replacement that simulates the behavior of `QUOTENAME`. You can't even use `sp_executeqsl` to do it, unfortunately. You'd still be concatenating the result of `QUOTENAME` into the statement. – madreflection Sep 19 '19 at 17:06
  • @EdPlunkett -- ok, i see ur point on removing the `finally` part after adding `using` and i will definitely be using parameters. this is a standalone worker process and still early in the stages of development. but i don't think that's the issue with the connection failing the 2nd time though, right? – gotmike Sep 19 '19 at 17:07
  • If you're calling this method within a loop, there's no need to create a new connection each time. Create a connection and pass it to this method. Put the `using` where the connection object is instantiated, not in methods that receive it (because they don't own it). – madreflection Sep 19 '19 at 17:10
  • 4
    @gotmike You should be disposing the SqlCommand too, btw. – 15ee8f99-57ff-4f92-890c-b56153 Sep 19 '19 at 17:10
  • Side non-welcoming note: the beauty of [MCVE] is that you don't need to show all crazy SQL injections you put into production code. You could have clean `SELECT * FROM MyTable` to demonstrate the problem and cut down on all unrelated "you don't #@$@ you SQL" comments... – Alexei Levenkov Sep 19 '19 at 17:14
  • @madreflection -- so the bigger picture here is that this is a relatively long-running process (log in database, download files, extract files, log in database, upload files, trigger stored procedure, add users, etc) -- database calls are required throughout the code, but long amounts of time pass between them. i was trying to be efficient by not holding a connection open for the entire time, but maybe that's the answer? seems inefficient if the entire process takes 20mins or so... – gotmike Sep 19 '19 at 17:14
  • 1
    @AlexeiLevenkov -- i started that way but wasn't sure if it was the type of call that could be causing the issue and the `ExecuteNonQuery` versus some other type of call... but duly noted. – gotmike Sep 19 '19 at 17:15
  • That's sensible, although, the advice transcends the context of loops. The point is that the method itself doesn't need to be responsible for creating connections. It's about separation of concerns and composability. – madreflection Sep 19 '19 at 17:16
  • @EdPlunkett -- would not disposing the `SqlCommand` cause the subsequent call to fail even if the `SqlConnection` is manually disposed or done so via `using`? – gotmike Sep 19 '19 at 17:17
  • @madreflection -- but i'm still not entirely sure what would cause the subsequent calls to actually fail. is there some sort of "minimum wait time" between calls after disposing a connection? – gotmike Sep 19 '19 at 17:18
  • I don't know about the message you're getting. Maybe if you're using some form of load balancing where the credentials have gotten out of sync (just a stab in the dark)? Other than that, I don't see why the same credentials would work sometimes but not always. – madreflection Sep 19 '19 at 17:21
  • @gotmike Not IIRC, and not in the test case I just wrote. You should just dispose the SqlCommand as a matter of good practice, because it implements IDisposable. It's one of the off-topic quibbles Alexander mentioned. – 15ee8f99-57ff-4f92-890c-b56153 Sep 19 '19 at 17:21
  • @gotmike I can't reproduce the error you're seeing. The only difference is I'm not creating users in the SQL, I'm just calling `select getdate()`. Does it work for you a second time if you change the SQL statement to `select getdate()`? You say "I had something like this" -- *how much* like that, exactly, was it? – 15ee8f99-57ff-4f92-890c-b56153 Sep 19 '19 at 17:28
  • 1
    @EdPlunkett -- actually with a very stripped down example, it seems to be working. i will debug a bit more and post an update when i find a solution to the larger problem. i guess i'm trying to follow the "minimal reproducible example" but clearly i omitted something important... – gotmike Sep 19 '19 at 17:47
  • 2
    @gotmike For what it's worth, the last several times I went to ask a question, by the time I had it whittled down to a minimal, reproducible example, I knew what the problem was and I didn't ask the question. I can look at code and *know* what these [irritating people](https://stackoverflow.com/users/424129/ed-plunkett) in the comments are going to say about it. – 15ee8f99-57ff-4f92-890c-b56153 Sep 19 '19 at 17:49
  • 2
    That's one of the reasons why we look for a minimal, reproducible example! (If only more people understood that.) – madreflection Sep 19 '19 at 17:50
  • totally understand. i have done the exact same thing many times. this time it just didn't happen... – gotmike Sep 19 '19 at 17:51

1 Answers1

0

SqlConnection implements IDisposable. You don't call dispose or close.

try{
 using (SqlConnection conn = new SqlConnection(strConnectionString))
    {

            conn.Open();
            string strSqlCommandText = $"CREATE USER {strUsername} for LOGIN {strUsername} WITH DEFAULT SCHEMA = [dbo];";
            SqlCommand sqlCommand = new SqlCommand(strSqlCommandText, conn);
            var sqlNonReader = sqlCommand.ExecuteNonQuery();
            if (sqlNonReader == -1) Utility.Notify($"User Added: {strUsername}");

    }}
catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
terrencep
  • 675
  • 5
  • 16