9

When writing some unit tests for our application, I stumbled upon some weird behaviour in EF6 (tested with 6.1 and 6.1.2): apparently it is impossible to repeatedly create and delete databases (same name/same connection string) within the same application context.

Test setup:

public class A
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class AMap : EntityTypeConfiguration<A>
{
    public AMap()
    {
        HasKey(a => a.Id);
        Property(a => a.Name).IsRequired().IsMaxLength().HasColumnName("Name");
        Property(a => a.Id).HasColumnName("ID");
    }
}

public class SomeContext : DbContext
{
    public SomeContext(DbConnection connection, bool ownsConnection) : base(connection, ownsConnection)
    {

    }

    public DbSet<A> As { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Configurations.Add(new AMap());
    }
}

[TestFixture]
public class BasicTest
{
    private readonly HashSet<string> m_databases = new HashSet<string>();

    #region SetUp/TearDown

    [TestFixtureSetUp]
    public void SetUp()
    {
        System.Data.Entity.Database.SetInitializer(
            new CreateDatabaseIfNotExists<SomeContext>());
    }


    [TestFixtureTearDown]
    public void TearDown()
    {
        foreach (var database in m_databases)
        {
            if (!string.IsNullOrWhiteSpace(database))
                DeleteDatabase(database);
        }
    }

    #endregion


    [Test]
    public void RepeatedCreateDeleteSameName()
    {
        var dbName = Guid.NewGuid().ToString();
        m_databases.Add(dbName);
        for (int i = 0; i < 2; i++)
        {
            Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
            Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
        }

        Console.WriteLine();
    }

    [Test]
    public void RepeatedCreateDeleteDifferentName()
    {
        for (int i = 0; i < 2; i++)
        {
            var dbName = Guid.NewGuid().ToString();
            if (m_databases.Add(dbName))
            {
                Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
                Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
            }
        }

        Console.WriteLine();
    }

    [Test]
    public void RepeatedCreateDeleteReuseName()
    {
        var testDatabases = new HashSet<string>();
        for (int i = 0; i < 3; i++)
        {
            var dbName = Guid.NewGuid().ToString();
            if (m_databases.Add(dbName))
            {
                testDatabases.Add(dbName);
                Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
                Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
            }
        }
        var repeatName = testDatabases.OrderBy(n => n).FirstOrDefault();
        Assert.IsTrue(CreateDatabase(repeatName), "failed to create database");
        Assert.IsTrue(DeleteDatabase(repeatName), "failed to delete database");

        Console.WriteLine();
    }

    #region Helpers

    private static bool CreateDatabase(string databaseName)
    {
        Console.Write("creating database '" + databaseName + "'...");
        using (var connection = CreateConnection(CreateConnectionString(databaseName)))
        {
            using (var context = new SomeContext(connection, false))
            {
                var a = context.As.ToList(); // CompatibleWithModel must not be the first call
                var result = context.Database.CompatibleWithModel(false);
                Console.WriteLine(result ? "DONE" : "FAIL");
                return result;
            }
        }
    }


    private static bool DeleteDatabase(string databaseName)
    {
        using (var connection = CreateConnection(CreateConnectionString(databaseName)))
        {
            if (System.Data.Entity.Database.Exists(connection))
            {
                Console.Write("deleting database '" + databaseName + "'...");
                var result = System.Data.Entity.Database.Delete(connection);
                Console.WriteLine(result ? "DONE" : "FAIL");
                return result;
            }
            return true;
        }
    }

    private static DbConnection CreateConnection(string connectionString)
    {
        return new SqlConnection(connectionString);
    }

    private static string CreateConnectionString(string databaseName)
    {
        var builder = new SqlConnectionStringBuilder
        {
            DataSource = "server",
            InitialCatalog = databaseName,
            IntegratedSecurity = false,
            MultipleActiveResultSets = false,
            PersistSecurityInfo = true,
            UserID = "username",
            Password = "password"
        };
        return builder.ConnectionString;
    }

    #endregion

}

RepeatedCreateDeleteDifferentName completes successfully, the other two fail. According to this, you cannot create a database with the same name, already used once before. When trying to create the database for the second time, the test (and application) throws a SqlException, noting a failed login. Is this a bug in Entity Framework or is this behaviour intentional (with what explanation)?

I tested this on a Ms SqlServer 2012 and Express 2014, not yet on Oracle. By the way: EF seems to have a problem with CompatibleWithModel being the very first call to the database.

Update: Submitted an issue on the EF bug tracker (link)

hoekki
  • 228
  • 1
  • 11
  • Is there a reason all your helpers are static methods? – timothyclifford Jan 29 '15 at 12:52
  • @timothyclifford Perhaps it's because they don't rely on any state, so they can be marked static? Is there a reason you're opposed to static methods? – spender Jan 29 '15 at 12:56
  • Generally just don't like static classes/methods when it comes to unit tests but this is just personal preference :) looking at the code it seems fine, will do some testing myself and see if I can find out more – timothyclifford Jan 29 '15 at 13:40
  • I admit, static helpers do not make a lot of sense here, although they do not hurt ;-) – hoekki Jan 29 '15 at 14:41
  • In our unit test suite, which is a bit more extensive than this example ;-), we use a helper class to provide centralised database access. This helps with switching between different test databases, even providers (we test against MsSql, Oracle, SqlCE). I admit to being a bit sloppy with putting together this example. – hoekki Jan 29 '15 at 14:49
  • @Eranga: Yes, this is about EF, an issue I ran across when testing my software. Created this test to make it easier for others to reproduce the issue. I took out the 'unit-testing' tag to prevent further confusion about this. – hoekki Feb 02 '15 at 08:52
  • Try adding a timeout between delete and create a new database. – Pawel Feb 02 '15 at 17:34
  • @Pawel: already thought of that and tried to get it to work with both a 2-times-ConnectionTimeout (~30secs) and a >10min delay between deletes and creates. No joy. But thanks for your input :) – hoekki Feb 04 '15 at 09:35

2 Answers2

4

Database initializers only run once per context per AppDomain. So if you delete the database at some arbitrary point they aren't going to automatically re-run and recreate the database. You can use DbContext.Database.Initialize(force: true) to force the initializer to run again.

Rowan Miller
  • 2,090
  • 15
  • 15
  • Thanks man, that solved my problem. I do not know how I could miss that. Keep up the good work with EF! But there still seems to be a dependency on the connection string as database creation did work with the same context within the same AppDomain but different database name. Nevertheless it works like a charm now. Cheers :) – hoekki Feb 11 '15 at 08:25
1

A few days ago I wrote integration tests that included DB access through EF6. For this, I had to create and drop a LocalDB database on each test case, and it worked for me.

I didn't use EF6 database initializer feature, but rather executed a DROP/CREATE DATABASE script, with the help of this post - I copied the example here:

using (var conn = new SqlConnection(@"Data Source=(LocalDb)\v11.0;Initial Catalog=Master;Integrated Security=True"))
{ 
    conn.Open();
    var cmd = new SqlCommand();
    cmd.Connection = conn;
    cmd.CommandText =  string.Format(@"
        IF EXISTS(SELECT * FROM sys.databases WHERE name='{0}')
        BEGIN
            ALTER DATABASE [{0}]
            SET SINGLE_USER
            WITH ROLLBACK IMMEDIATE
            DROP DATABASE [{0}]
        END

        DECLARE @FILENAME AS VARCHAR(255)

        SET @FILENAME = CONVERT(VARCHAR(255), SERVERPROPERTY('instancedefaultdatapath')) + '{0}';

        EXEC ('CREATE DATABASE [{0}] ON PRIMARY 
            (NAME = [{0}], 
            FILENAME =''' + @FILENAME + ''', 
            SIZE = 25MB, 
            MAXSIZE = 50MB, 
            FILEGROWTH = 5MB )')", 
        databaseName);

    cmd.ExecuteNonQuery();
}

The following code was responsible for creating database objects according to the model:

var script = objectContext.CreateDatabaseScript();

using ( var command = connection.CreateCommand() )
{
    command.CommandType = CommandType.Text;
    command.CommandText = script;

    connection.Open();
    command.ExecuteNonQuery();
}

There was no need to change database name between the tests.

felix-b
  • 8,178
  • 1
  • 26
  • 36
  • Thanks for your answer. Your version of a workaround should work, in fact, I have implemented something similar for my application. However, this does not really answer my question. In my opinion, an ORM that forces developers to write and execute hard-coded SQL scripts is not doing its job. You can, of course, do so for performance reasons, but this should not be a requirement. Especially if there are functions provided by the framework that are intended to do the job in the first place... – hoekki Feb 10 '15 at 16:01
  • Part 2: In my research on this, I found various solutions which require e.g. the database to be set to single-user mode to kill existing connections, or call ClearAllPools. But the real problem is not with deleting databases, rather than with (re-)creating them. Not being able to create a database that does not exist (anymore) is whats baffling me. As mentioned earlier, I use EF not only on MsSQL but on Oracle and SqlCE aswell (and within the same application context)... – hoekki Feb 10 '15 at 16:02
  • Part 3: So I'd really like to use EF functionality as far as possible and not have to fall back to SQL scripts for each database provider, for each version, ... and so on. – hoekki Feb 10 '15 at 16:03
  • 1
    damn those comment limitations – hoekki Feb 10 '15 at 16:03
  • OK, I see. You didn't look for a way to just make it work, but rather for the right way of doing it. Yes, EF is not good at layer abstraction, sometimes I think that abstraction is not among its design goals. This is exactly the issue I'm currently facing. My current solution is to provide an API to business logic which is totally abstracted from underlying ORM. Then, provide an adaptation layer to a specific ORM. Anything that depends on EF resides in the adaptation layer. Here is another example: [http://stackoverflow.com/a/28403546/4544845](http://stackoverflow.com/a/28403546/4544845) – felix-b Feb 10 '15 at 18:01