8

I am trying to write an easy to understand DBContext class that takes a custom connection string, can run migrations, and that I allows me to generate migrations using Package Manager.

I seem to be going around in circles.

I have been able to get it working using code that feels awful to me. I documented this in my answer to This question on connection string and migrations.

Radek's answer looks much better than mine, but I find that when I implement it and then try and create a migration in Package Manager I get the message

The target context 'DataLayer.Context' is not constructible. Add a default constructor or provide an implementation of IDbContextFactory.

Where DataLayer.Context is my context class.

I don't want to provide an implementation of IDbContextFactory ( and Radek's answer seems to indicate it isn't needed )

UPDATE:

I can generate a migration if I include a constructor with no parameter. For example

public Context() : base("ConnectionStringName") { }

For my context creation I pass the name of the connection string in app.config

public Context(string connString) : base(connString)
{
    Database.SetInitializer(new CustomInitializer());
    Database.Initialize(true);
}

At last I am able to both generate migrations, and run migrations for databases that the user selects.

HOWEVER: When I drop the database and then run my app I have problems.

The initialiser code I am using, from the link above is

public class CustomInitializer : IDatabaseInitializer<Context>
{       
    public void InitializeDatabase(Context context)
    {
        try
        {
            if (!context.Database.Exists())
            {
                context.Database.Create();
            }
            else
            {
                if (!context.Database.CompatibleWithModel(false))  
                {
                    var configuration = new Configuration();
                    var migrator = new DbMigrator(configuration);
                    migrator.Configuration.TargetDatabase =
                        new DbConnectionInfo(context.Database.Connection.ConnectionString);
                    IEnumerable<string> migrations = migrator.GetPendingMigrations();
                    foreach (string migration in migrations)
                    {
                        var scriptor = new MigratorScriptingDecorator(migrator);
                        string script = scriptor.ScriptUpdate(null, migration);
                        context.Database.ExecuteSqlCommand(script);
                    }
                }
            }
        }
        catch (Exception ex)
        {
        }
    }       
}

When I drop the database a new one gets created but it has no tables. That would be because my table creation code is all in my first migration.

So the code inside the !context.Database.CompatibleWithModel(false) condition does not run.

However alas, the code also does not run the second time around when it should have the metadatamodel.

Now to try and get the migration running...

SADNESS: No with Radek's custom initializer so far.

From NSGaga's comments I have resorted to exiting the application if I change connection.

var master = new myMDIForm();
master.ConnectionType = connectionType;   // being an enum of the different connection names in app.config
while (master.ConnectionType != ConnectionType.None )
{
   Application.Run(master);
}
Community
  • 1
  • 1
Kirsten
  • 15,730
  • 41
  • 179
  • 318
  • this looks a similar question to this solution answer http://stackoverflow.com/questions/15504465/entityframework-code-first-custom-connection-string-and-migrations/16133150#16133150 – phil soady Apr 22 '13 at 06:56

2 Answers2

7

(note: I have no idea about your custom initializer, I just saw that - this is a general solution to the connection caching problem and migration initializer - but should work with variations)


This is what works for me - and based on the buggy behavior that I described here.
static string _connection;
public MyContext()
    : base(_connection ?? "DefaultConection")
{
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyNamespace.Migrations.Configuration>());
}
public MyContext(string connection)
    : base(connection)
{
    _connection = connection;
    Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyNamespace.Migrations.Configuration>());
}

Few points:

1) Define static connection in your context - and as you set a 'new one' change it to hold the latest value. That helps keeping things in synchronization - no matter who accesses the DbContext has the same one (it's similar in concept to Factory just easier on the eyes,

2) Problem with Migrations is - that MigrateDatabaseToLatestVersion caches the connection (and entire configuration internally) in a readonly field internally - and even if you change it in your DbContext, or whatever you set outside, it gets 'out of sync`.

There is no way getting around that but to make a 'new initializer'.

3) What @Radek discovered was actually the solution for that problem - and in line with (2). I just removed the Initialize(true) as it's unnecessary - that gets called when 'the time is right'.


That's about it.

With this I can now flip my connections around the clock - and as I want.
(meaning I can change connection at runtime - migrate/create two, or more, databases and keep changing connections and working on them simultaneously)

And this is the code I actually use to cycle between connections...

for (var flip = true; true; flip = !flip)
{
    using (var db = new MyContext(flip ? "Name=DefaultConnection" : "Name=OtherConnection"))
    {
        // usual db code
    }
}

The key is to set initializer each time you set the connection.
The old one is still around - and the old connection (you can't delete the Db while app is running)

Community
  • 1
  • 1
NSGaga-mostly-inactive
  • 14,052
  • 3
  • 41
  • 51
  • After I made your change, the application became very slow to run. – Kirsten Apr 10 '13 at 23:35
  • that doesn't make any sense @kirsteng :) - it's not doing anything in that sense - except it's actually working - so it initializes and migrates. Check again - this actually is a solution to all the problems as it covers the 'why' this stuff happens - I just posted it for you as I see that you're sturggling. It's up to you whether you'd like to use it or not. Cheers. – NSGaga-mostly-inactive Apr 10 '13 at 23:41
  • And of course you gotta work out your specifics - I don't know what custom initializer does, so you probably still have work to do. But this is the approach that you should be taking - and within your initializer. – NSGaga-mostly-inactive Apr 10 '13 at 23:43
  • I think you are right. I can create a database and run migrations with different connections using your method. However I am experiencing some strange issues running on the different connections. I need a bit more time. – Kirsten Apr 10 '13 at 23:56
  • 1
    of course - I'll be around :) - just keep it within how EF works - it's the 'limitation' - they jumped from one-connection to 'multiple' w/o actually rewriting all the code. So they support it - but with problems. There might be issues, as it's not ideal. First initializer needs to 'work with' connections well. Btw. my custom initializer works ok, you have that link somewhere I think so you can take a look. Then what happens is that all connections 'stay alive' for the duration of the AppDomain. I don't expect bigger issues though. We can check the source if you run into something. Good night. – NSGaga-mostly-inactive Apr 11 '13 at 00:01
  • Yup - my app behaves strangely on the alternate connection. I think this is why I experienced slowness. But there are other issues I am yet to get to the bottom of. We need a way to get the old connections out of AppDomain. – Kirsten Apr 11 '13 at 00:21
  • I don't think we can remove them - but maybe give DbContext the ready DbConnection - and then dispose of that later yourself, the only thing that pops to mind – NSGaga-mostly-inactive Apr 11 '13 at 00:35
0

After much shuffling around, I got this to work:

Public Class BlogContext
  Inherits DbContext

  Public Sub New()
    Database.Connection.ConnectionString = Utils.SqlCeConnection.ConnectionString
  End Sub



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

    Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of BlogContext, Migrations.Configuration))
    Database.Initialize(True)
  End Sub



  Public Shared Function Create() As BlogContext
    Return New BlogContext(Utils.SqlCeConnection)
  End Function



  Public Property Blogs As DbSet(Of Blog)
End Class

Here's the Configuration class. Note the disabled AutomaticMigrations:

Namespace Migrations
  Friend NotInheritable Class Configuration
    Inherits DbMigrationsConfiguration(Of BlogContext)

    Public Sub New()
      Me.AutomaticMigrationsEnabled = False
    End Sub

    Protected Overrides Sub Seed(Context As BlogContext)
    End Sub
  End Class
End Namespace

And here's the SqlCeConnection helper class, where the database location can be set at runtime:

Public Class Utils
  Public Shared ReadOnly Property SqlCeConnection As SqlCeConnection
    Get
      Return New SqlCeConnection(ConnectionString)
    End Get
  End Property



  Public Shared ReadOnly Property SqlCeConnection(DataSource As String) As SqlCeConnection
    Get
      Return New SqlCeConnection(ConnectionString(DataSource))
    End Get
  End Property



  Private Shared ReadOnly Property ConnectionString As String
    Get
      Return ConnectionString(DatabasePath)
    End Get
  End Property



  Private Shared ReadOnly Property ConnectionString(DataSource) As String
    Get
      With New SqlCeConnectionStringBuilder
        .MaxDatabaseSize = 4091
        .MaxBufferSize = 1024
        .CaseSensitive = False
        .FlushInterval = 1
        .DataSource = DataSource
        .FileMode = "Read Write"
        .Encrypt = False
        .Enlist = True

        Return .ConnectionString
      End With
    End Get
  End Property



  Public Shared ReadOnly Property DatabasePath As String
    Get
      Return Path.Combine(DatabaseFolder, DatabaseFile)
    End Get
  End Property



  Public Shared ReadOnly Property DatabaseFile As String
    Get
      Return "MigrationsDemo.sdf"
    End Get
  End Property



  Public Shared ReadOnly Property DatabaseFolder As String
    Get
      DatabaseFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MigrationsDemo")

      If Not Directory.Exists(DatabaseFolder) Then
        Directory.CreateDirectory(DatabaseFolder)
      End If
    End Get
  End Property
End Class

HTH

InteXX
  • 6,135
  • 6
  • 43
  • 80