Tuesday, June 21, 2016

PowerShell script to execute an Entity Framework Migration

I got sick and tired of coming up with hacks for including Entity Framework migrations as part of my deployment processes, so I came up with a much cleaner hack in the form of a PowerShell script that can be used to execute a database migration given an assembly and a connection string. This script does have one caveat though: for some reason if it's run more than once in a given PowerShell session, it causes a stack overflow exception, and I haven't been able to figure out why. I guess I'll have to leave it as an exercise to you. Have at it:

Param(
    [ValidateNotNullOrEmpty()][string]$assemblyFile,
[ValidateNotNullOrEmpty()][string]$connectionString
#    [ValidateNotNullOrEmpty()][string]$server,
#    [ValidateNotNullOrEmpty()][string]$databaseName,
#    [ValidateNotNullOrEmpty()][string]$userName,
#    [ValidateNotNullOrEmpty()][string]$password
)

$assemblyFolder = [System.IO.Path]::GetDirectoryName($assemblyFile)

$OnAssemblyResolve = [System.ResolveEventHandler] {
  param($sender, $e)

  # First load the assemblies already in our app domain
  foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
  {
    if ($a.FullName -eq $e.Name)
    {
      return $a
    }
  }

  $fn = Join-Path $assemblyFolder "$($e.Name.Split(',')[0]).dll"

  if (Test-Path $fn)
  {
      $ass = [Reflection.Assembly]::LoadFile($fn)
      return $ass
  }

  return $null
}

[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

Set-Location ($assemblyFolder)

[System.Reflection.Assembly] $assembly = [System.Reflection.Assembly]::LoadFile($assemblyFile)

$types = $null

try
{
    $types = $assembly.GetTypes()
}
catch [Exception]
{
    $ErrorMessage = $_.Exception.Message

    throw "Failed to load types from assembly"
}

$configurationType = [System.Type]::GetType("System.Data.Entity.Migrations.DbMigrationsConfiguration, EntityFramework")
$migratorType = [System.Type]::GetType("System.Data.Entity.Migrations.DbMigrator, EntityFramework")
$connectionInfoType = [System.Type]::GetType("System.Data.Entity.Infrastructure.DbConnectionInfo, EntityFramework")

if ($configurationType -eq $null -or $migratorType -eq $null -or $connectionInfoType -eq $null)
{
    throw "Failed to load entity framework"
}

$migrationConfigurationTypes = @($types | ? { $configurationType.IsAssignableFrom($_) })

if ($migrationConfigurationTypes.Length -ne 1)
{
    throw "Failed to find single migration type for Entity framework in the assembly to migrate, found $($migrationConfigurationTypes.Length) migration configuration types"
}

Write-Warning "If you see an error similar to 'Could not find a connection string named [connection string name] in the application config', try changing your DbContext descendent to use the DbContext('connection string name') constructor syntax instead of DbContext('name=connection string name'). This is a known bug in EntityFramework."

#$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
# Need to use psbase, otherwise these properties throw an exception
#$builder.psbase.DataSource = $server
#$builder.psbase.InitialCatalog = $databaseName
#$builder.psbase.UserID = $userName
#$builder.psbase.Password = $password

#$connectionString = $builder.ToString()

$configuration = [System.Activator]::CreateInstance($migrationConfigurationTypes[0])
$connectionInfo = New-Object $connectionInfoType ($connectionString, "System.Data.SqlClient")
$configuration.TargetDatabase = $connectionInfo
$migrator = [System.Activator]::CreateInstance($migratorType, @($configuration))
$migrator.Update()

Write-Host 'Successfully migrated database'


No comments: