Search code examples
entity-frameworkef-code-firstconnection-stringcode-firstentity-framework-migrations

Migration with dynamic connection string in Entity Framework code-first (the connection string from text file)


I'm using EF6 code-first and at the first, I put the connection string in a text file called 'Settings.txt'

The data in the 'Settings.txt' file is

DataProvider: sqlserver
DataConnectionString: Data Source=.\SQLEXPRESS;Initial Catalog=MyDb;Integrated Security=True;Persist Security Info=False;Enlist=False;

Here what I use for the dbContext class:

public class DbDataContext : BaseDbContext
{
    static DbDataContext()
    {
        Database.SetInitializer(new ContextInitializer());
    }

    public DbDataContext():base() { }

    public DbDataContext(string nameOrConnectionString)
        : base(nameOrConnectionString) { }

    ...    
}

[DbConfigurationType(typeof(MyDbConfiguration))]
public abstract partial class BaseDbContext : DbContext, IDbContext
{
    public BaseDbContext() : this(GetConnectionString())
    { }

    public BaseDbContext(string nameOrConnectionString) : base(nameOrConnectionString)
    { }

    public static string GetConnectionString()
    {
        if (DataSettings.DataSettings.Current.IsValid())
        {
            return DataSettings.DataSettings.Current.DataConnectionString;
        }

        throw Error.Application("A connection string could not be resolved for the parameterless constructor of the derived DbContext. Either the database is not installed, or the file 'Settings.txt' does not exist or contains invalid content.");
    }
}

public class MyDbConfiguration : DbConfiguration
{
    public MyDbConfiguration()
    {
        IEfDataProvider provider = null;

        try
        {
            provider = (new EfDataProviderFactory(DataSettings.DataSettings.Current).LoadDataProvider()) as IEfDataProvider;
        }
        catch {
        }

        if (provider != null)
        {
            base.SetDefaultConnectionFactory(provider.GetConnectionFactory());
        }
    }
}

public partial class EfDataProviderFactory : DataProviderFactory
{
    public EfDataProviderFactory()
        : this(DataSettings.DataSettings.Current){ }

    public EfDataProviderFactory(DataSettings.DataSettings settings)
        : base(settings) { }

    public override IDataProvider LoadDataProvider()
    {
        var providerName = Settings.DataProvider;

        if (providerName.IsEmpty())
        {
            throw new Exception("Data Settings doesn't contain a providerName");
        }

        switch (providerName.ToLowerInvariant())
        {
            case "sqlserver":
                return new SqlServerDataProvider();

            case "sqlserverce":
                return new SqlServerCeDataProvider();

            default:
                throw new Exception(string.Format("Unsupported dataprovider name: {0}", providerName));
        }
    }
}

public class SqlServerDataProvider : IEfDataProvider
{
    public virtual IDbConnectionFactory GetConnectionFactory()
    {
        return new SqlConnectionFactory();
    }

    public bool StoredProceduresSupported
    {
        get { return false; }
    }

    public DbParameter GetParameter()
    {
        return new SqlParameter();
    }

    public string ProviderInvariantName
    {
        get { return "System.Data.SqlClient"; }
    }
}

I use a static function in 'BaseDbContext' class called 'GetConnectionString()'

This function just for return the connection string from the text file. This behavior is working very well at runtime, but it not working when adding a migration.

This is the problem: how can I add a migration by this way, knowing that when I put the connection string directly in the function like this

    public static string GetConnectionString()
    {
         return (@"Data Source=.\\SQLEXPRESS;Initial Catalog=MyDb;Integrated Security=True;Persist Security Info=False;Enlist=False;");
    }

the Add-Migration command is working

How can I solve this problem without forcing the connection string in the code?


Solution

  • I solved this, the problem occurs in getting the file path (text file) in design-mode or even unit tests

    string filePath = Path.Combine(MapPath("~/App_Data/"), "Settings.txt");
    

    public static string MapPath(string path)
        {
                path = path.Replace("~/", "").TrimStart('/').Replace('/', '\\');
    
                var testPath = Path.Combine(baseDirectory, path);
    
                var dir = FindSolutionRoot(baseDirectory);
    
                if (dir != null)
                    {
                        baseDirectory = Path.Combine(dir.FullName, "MyProjectName.WebAPI");
                        testPath = Path.Combine(baseDirectory, path);
    
    
                return testPath;
            }
        }
    

    private static DirectoryInfo FindSolutionRoot(string currentDir)
        {
            var dir = Directory.GetParent(currentDir);
            while (true)
            {
                if (dir == null || IsSolutionRoot(dir))
                    break;
    
                dir = dir.Parent;
            }
    
            return dir;
        }
    
        private static bool IsSolutionRoot(DirectoryInfo dir)
        {
            return File.Exists(Path.Combine(dir.FullName, "MySolutionName.sln"));
        }
    

    and by this, we can get the file path in runtime-mode