diff --git a/EFCore.sln b/EFCore.sln index 2f9c6c8e04..af923627fa 100644 --- a/EFCore.sln +++ b/EFCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26214.1 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore", "src\EFCore\EFCore.csproj", "{715C38E9-B2F5-4DB2-8025-0C6492DEBDD4}" EndProject @@ -69,6 +69,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Benchmarks.EFCore", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Benchmarks.EF6", "test\EFCore.Benchmarks.EF6\EFCore.Benchmarks.EF6.csproj", "{477EBF1E-A4B8-4D60-8681-5D6D5BB42CE1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-ef", "src\dotnet-ef\dotnet-ef.csproj", "{2D66A1DA-D102-4DD9-960B-7D863BBB53DE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ef", "src\ef\ef.csproj", "{4F7C93F3-A30F-4061-804C-32293DC256A1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Tools", "src\EFCore.Tools\EFCore.Tools.csproj", "{87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Tools.DotNet", "src\EFCore.Tools.DotNet\EFCore.Tools.DotNet.csproj", "{31ED3EA7-8270-478D-935D-0067BD7935B7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-ef.Tests", "test\dotnet-ef.Tests\dotnet-ef.Tests.csproj", "{27018CE2-C235-439C-80F2-C573C8904892}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ef.Tests", "test\ef.Tests\ef.Tests.csproj", "{935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -199,6 +211,30 @@ Global {477EBF1E-A4B8-4D60-8681-5D6D5BB42CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {477EBF1E-A4B8-4D60-8681-5D6D5BB42CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {477EBF1E-A4B8-4D60-8681-5D6D5BB42CE1}.Release|Any CPU.Build.0 = Release|Any CPU + {2D66A1DA-D102-4DD9-960B-7D863BBB53DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D66A1DA-D102-4DD9-960B-7D863BBB53DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D66A1DA-D102-4DD9-960B-7D863BBB53DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D66A1DA-D102-4DD9-960B-7D863BBB53DE}.Release|Any CPU.Build.0 = Release|Any CPU + {4F7C93F3-A30F-4061-804C-32293DC256A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F7C93F3-A30F-4061-804C-32293DC256A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F7C93F3-A30F-4061-804C-32293DC256A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F7C93F3-A30F-4061-804C-32293DC256A1}.Release|Any CPU.Build.0 = Release|Any CPU + {87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D}.Release|Any CPU.Build.0 = Release|Any CPU + {31ED3EA7-8270-478D-935D-0067BD7935B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31ED3EA7-8270-478D-935D-0067BD7935B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31ED3EA7-8270-478D-935D-0067BD7935B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31ED3EA7-8270-478D-935D-0067BD7935B7}.Release|Any CPU.Build.0 = Release|Any CPU + {27018CE2-C235-439C-80F2-C573C8904892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27018CE2-C235-439C-80F2-C573C8904892}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27018CE2-C235-439C-80F2-C573C8904892}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27018CE2-C235-439C-80F2-C573C8904892}.Release|Any CPU.Build.0 = Release|Any CPU + {935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {935B51B9-A9B9-4DA2-93A2-663D3BCEAA83}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -235,5 +271,11 @@ Global {E3146D04-6E87-41C1-A2E3-F2D7CDFB9B99} = {258D5057-81B9-40EC-A872-D21E27452749} {049A9748-B3CE-4412-B455-F7108B3BA239} = {258D5057-81B9-40EC-A872-D21E27452749} {477EBF1E-A4B8-4D60-8681-5D6D5BB42CE1} = {258D5057-81B9-40EC-A872-D21E27452749} + {2D66A1DA-D102-4DD9-960B-7D863BBB53DE} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} + {4F7C93F3-A30F-4061-804C-32293DC256A1} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} + {87ADBDB5-CA57-4EAB-9A8A-5E89480C9C6D} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} + {31ED3EA7-8270-478D-935D-0067BD7935B7} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} + {27018CE2-C235-439C-80F2-C573C8904892} = {258D5057-81B9-40EC-A872-D21E27452749} + {935B51B9-A9B9-4DA2-93A2-663D3BCEAA83} = {258D5057-81B9-40EC-A872-D21E27452749} EndGlobalSection EndGlobal diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index b153ab1515..504d23066a 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -1,4 +1,16 @@ { + "adx": { + "rules": [ + "AdxVerificationCompositeRule" + ], + "packages": { + "Microsoft.EntityFrameworkCore.Tools.DotNet": { + "packageTypes": [ + "DotnetCliTool" + ] + } + } + }, "Default": { "rules": [ "DefaultCompositeRule" diff --git a/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.csproj b/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.csproj new file mode 100644 index 0000000000..5b650070c6 --- /dev/null +++ b/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.csproj @@ -0,0 +1,28 @@ + + + + + + netcoreapp1.0 + $(MSBuildThisFileDirectory)$(MSBuildProjectName).nuspec + true + false + + + + + + + + + + version=$(PackageVersion);configuration=$(Configuration) + + + + + + + + + \ No newline at end of file diff --git a/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.nuspec b/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.nuspec new file mode 100644 index 0000000000..056048de47 --- /dev/null +++ b/src/EFCore.Tools.DotNet/EFCore.Tools.DotNet.nuspec @@ -0,0 +1,31 @@ + + + + Microsoft.EntityFrameworkCore.Tools.DotNet + $version$ + Microsoft + false + Entity Framework Core .NET Command Line Tools. Includes dotnet-ef. + Entity Framework Core,entity-framework-core,EF,Data,O/RM + + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/EFCore.Tools.DotNet/prefercliruntime b/src/EFCore.Tools.DotNet/prefercliruntime new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/EFCore.Tools/EFCore.Tools.csproj b/src/EFCore.Tools/EFCore.Tools.csproj new file mode 100644 index 0000000000..109749ebca --- /dev/null +++ b/src/EFCore.Tools/EFCore.Tools.csproj @@ -0,0 +1,28 @@ + + + + + + net451;netcoreapp1.0 + $(MSBuildThisFileDirectory)$(MSBuildProjectName).nuspec + true + true + false + + + + + + + + + version=$(PackageVersion);configuration=$(Configuration) + + + + + + + + + \ No newline at end of file diff --git a/src/EFCore.Tools/EFCore.Tools.nuspec b/src/EFCore.Tools/EFCore.Tools.nuspec new file mode 100644 index 0000000000..2b7ada242b --- /dev/null +++ b/src/EFCore.Tools/EFCore.Tools.nuspec @@ -0,0 +1,30 @@ + + + + Microsoft.EntityFrameworkCore.Tools + $version$ + Microsoft + false + true + Entity Framework Core Package Manager Console Tools. Includes Scaffold-DbContext, Add-Migration, and Update-Database. + Entity Framework Core,entity-framework-core,EF,Data,O/RM + true + + + + + + + + + + + + + + + + + + + diff --git a/src/EFCore.Tools/lib/net451/_._ b/src/EFCore.Tools/lib/net451/_._ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/EFCore.Tools/lib/netstandard1.3/_._ b/src/EFCore.Tools/lib/netstandard1.3/_._ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psd1 b/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psd1 new file mode 100644 index 0000000000..72c6ce645c --- /dev/null +++ b/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psd1 @@ -0,0 +1,87 @@ +@{ + # Script module or binary module file associated with this manifest + ModuleToProcess = 'EntityFrameworkCore.PowerShell2.psm1' + + # Version number of this module. + ModuleVersion = '1.1.0' + + # ID used to uniquely identify this module + GUID = '2de7c7fd-c848-41d7-8634-37fed4d3bb36' + + # Author of this module + Author = 'Entity Framework Team' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) .NET Foundation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Entity Framework Core Package Manager Console Tools' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '1.0' + + # Name of the Windows PowerShell host required by this module + PowerShellHostName = 'Package Manager Host' + + # Minimum version of the Windows PowerShell host required by this module + PowerShellHostVersion = '1.2' + + # Minimum version of the .NET Framework required by this module + DotNetFrameworkVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '' + + # Processor architecture (None, X86, Amd64, IA64) required by this module + ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = 'NuGet' + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module + ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in ModuleToProcess + NestedModules = @() + + # Functions to export from this module + FunctionsToExport = ( + 'Add-Migration', + 'Drop-Database', + 'Enable-Migrations', + 'Remove-Migration', + 'Scaffold-DbContext', + 'Script-Migration', + 'Update-Database' + ) + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module + AliasesToExport = @() + + # List of all modules packaged with this module + ModuleList = @() + + # List of all files packaged with this module + FileList = @() + + # Private data to pass to the module specified in ModuleToProcess + PrivateData = '' +} diff --git a/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psm1 b/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psm1 new file mode 100644 index 0000000000..3c25dd1beb --- /dev/null +++ b/src/EFCore.Tools/tools/EntityFrameworkCore.PowerShell2.psm1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = 'Stop' + +$versionErrorMessage = 'The Entity Framework Core Package Manager Console Tools don''t support PowerShell version ' + + "$($PSVersionTable.PSVersion). Upgrade to PowerShell version 3.0 or higher, restart Visual Studio, and try again." + +function Add-Migration +{ + WarnIfEF6 'Add-Migration' + + throw $versionErrorMessage +} + +function Drop-Database +{ + throw $versionErrorMessage +} + +function Enable-Migrations +{ + WarnIfEF6 'Enable-Migrations' + + throw $versionErrorMessage +} + +function Remove-Migration +{ + throw $versionErrorMessage +} + +function Scaffold-DbContext +{ + throw $versionErrorMessage +} + +function Script-Migration +{ + throw $versionErrorMessage +} + +function Update-Database +{ + WarnIfEF6 'Update-Database' + + throw $versionErrorMessage +} + +function WarnIfEF6($cmdlet) +{ + if (Get-Module 'EntityFramework') + { + Write-Warning "Both Entity Framework Core and Entity Framework 6 are installed. The Entity Framework Core tools are running. Use 'EntityFramework\$cmdlet' for Entity Framework 6." + } +} \ No newline at end of file diff --git a/src/EFCore.Tools/tools/EntityFrameworkCore.psd1 b/src/EFCore.Tools/tools/EntityFrameworkCore.psd1 new file mode 100644 index 0000000000..ab8189dad4 --- /dev/null +++ b/src/EFCore.Tools/tools/EntityFrameworkCore.psd1 @@ -0,0 +1,87 @@ +@{ + # Script module or binary module file associated with this manifest + ModuleToProcess = 'EntityFrameworkCore.psm1' + + # Version number of this module. + ModuleVersion = '1.1.0' + + # ID used to uniquely identify this module + GUID = 'c126fb40-c0f1-43ae-8dd0-06bb50512eb2' + + # Author of this module + Author = 'Entity Framework Team' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) .NET Foundation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Entity Framework Core Package Manager Console Tools' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '3.0' + + # Name of the Windows PowerShell host required by this module + PowerShellHostName = 'Package Manager Host' + + # Minimum version of the Windows PowerShell host required by this module + PowerShellHostVersion = '1.2' + + # Minimum version of the .NET Framework required by this module + DotNetFrameworkVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '' + + # Processor architecture (None, X86, Amd64, IA64) required by this module + ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = 'NuGet' + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module + ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in ModuleToProcess + NestedModules = @() + + # Functions to export from this module + FunctionsToExport = ( + 'Add-Migration', + 'Drop-Database', + 'Enable-Migrations', + 'Remove-Migration', + 'Scaffold-DbContext', + 'Script-Migration', + 'Update-Database' + ) + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module + AliasesToExport = @() + + # List of all modules packaged with this module + ModuleList = @() + + # List of all files packaged with this module + FileList = @() + + # Private data to pass to the module specified in ModuleToProcess + PrivateData = '' +} diff --git a/src/EFCore.Tools/tools/EntityFrameworkCore.psm1 b/src/EFCore.Tools/tools/EntityFrameworkCore.psm1 new file mode 100644 index 0000000000..247046ee7a --- /dev/null +++ b/src/EFCore.Tools/tools/EntityFrameworkCore.psm1 @@ -0,0 +1,1102 @@ +$ErrorActionPreference = 'Stop' + +# +# Add-Migration +# + +Register-TabExpansion Add-Migration @{ + OutputDir = { <# Disabled. Otherwise, paths would be relative to the solution directory. #> } + Context = { param($x) GetContextTypes $x.Project $x.StartupProject $x.Environment } + Project = { GetProjects } + StartupProject = { GetProjects } +} + +<# +.SYNOPSIS + Adds a new migration. + +.DESCRIPTION + Adds a new migration. + +.PARAMETER Name + The name of the migration. + +.PARAMETER OutputDir + The directory (and sub-namespace) to use. Paths are relative to the project directory. Defaults to "Migrations". + +.PARAMETER Context + The DbContext type to use. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + Remove-Migration + Update-Database + about_EntityFrameworkCore +#> +function Add-Migration +{ + [CmdletBinding(PositionalBinding = $false)] + param( + [Parameter(Position = 0, Mandatory = $true)] + [string] $Name, + [string] $OutputDir, + [string] $Context, + [string] $Environment, + [string] $Project, + [string] $StartupProject) + + WarnIfEF6 'Add-Migration' + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + $params = 'migrations', 'add', $Name, '--json' + + if ($OutputDir) + { + $params += '--output-dir', $OutputDir + } + + $params += GetParams $Context $Environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $result = (EF $dteProject $dteStartupProject $params) -join "`n" | ConvertFrom-Json + Write-Output 'To undo this action, use Remove-Migration.' + + $dteProject.ProjectItems.AddFromFile($result.migrationFile) | Out-Null + $DTE.ItemOperations.OpenFile($result.migrationFile) | Out-Null + ShowConsole + + $dteProject.ProjectItems.AddFromFile($result.metadataFile) | Out-Null + + $dteProject.ProjectItems.AddFromFile($result.snapshotFile) | Out-Null +} + +# +# Drop-Database +# + +Register-TabExpansion Drop-Database @{ + Context = { param($x) GetContextTypes $x.Environment $x.Project $x.StartupProject } + Project = { GetProjects } + StartupProject = { GetProjects } +} + +<# +.SYNOPSIS + Drops the database. + +.DESCRIPTION + Drops the database. + +.PARAMETER Context + The DbContext to use. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + Update-Database + about_EntityFrameworkCore +#> +function Drop-Database +{ + [CmdletBinding(PositionalBinding = $false, SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param([string] $Context, [string] $Environment, [string] $Project, [string] $StartupProject) + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + if (IsUWP $dteProject) + { + throw 'Drop-Database shouldn''t be used with Universal Windows Platform apps. Instead, call ' + + 'DbContext.Database.EnsureDeleted() at runtime.' + } + + $params = 'dbcontext', 'info', '--json' + $params += GetParams $Context $Environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $info = (EF $dteProject $dteStartupProject $params) -join "`n" | ConvertFrom-Json + + if ($PSCmdlet.ShouldProcess("database '$($info.databaseName)' on server '$($info.dataSource)'")) + { + $params = 'database', 'drop', '--force' + $params += GetParams $Context $Environment + + EF $dteProject $dteStartupProject $params -skipBuild + } +} + +# +# Enable-Migrations (Obsolete) +# + +function Enable-Migrations +{ + WarnIfEF6 'Update-Database' + Write-Warning 'Enable-Migrations is obsolete. Use Add-Migration to start using Migrations.' +} + +# +# Remove-Migration +# + +Register-TabExpansion Remove-Migration @{ + Context = { param($x) GetContextTypes $x.Environment $x.Project $x.StartupProject } + Project = { GetProjects } + StartupProject = { GetProjects } +} + +<# +.SYNOPSIS + Removes the last migration. + +.DESCRIPTION + Removes the last migration. + +.PARAMETER Force + Don't check to see if the migration has been applied to the database. Always implied on UWP apps. + +.PARAMETER Context + The DbContext to use. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + Add-Migration + about_EntityFrameworkCore +#> +function Remove-Migration +{ + [CmdletBinding(PositionalBinding = $false)] + param([switch] $Force, [string] $Context, [string] $Environment, [string] $Project, [string] $StartupProject) + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + if (IsUWP $dteStartupProject) + { + $Force = [switch]::Present + } + + $params = 'migrations', 'remove', '--json' + + if ($Force) + { + $params += '--force' + } + + $params += GetParams $Context $Environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $result = (EF $dteProject $dteStartupProject $params) -join "`n" | ConvertFrom-Json + + $result | %{ + $projectItem = GetProjectItem $dteProject $_ + if ($projectItem) + { + $projectItem.Remove() + } + } +} + +# +# Scaffold-DbContext +# + +Register-TabExpansion Scaffold-DbContext @{ + Provider = { param($x) GetProviders $x.Project } + Project = { GetProjects } + StartupProject = { GetProjects } + OutputDir = { <# Disabled. Otherwise, paths would be relative to the solution directory. #> } +} + +<# +.SYNOPSIS + Scaffolds a DbContext and entity types for a database. + +.DESCRIPTION + Scaffolds a DbContext and entity types for a database. + +.PARAMETER Connection + The connection string to the database. + +.PARAMETER Provider + The provider to use. (E.g. Microsoft.EntityFrameworkCore.SqlServer) + +.PARAMETER OutputDir + The directory to put files in. Paths are relaive to the project directory. + +.PARAMETER Context + The name of the DbContext to generate. + +.PARAMETER Schemas + The schemas of tables to generate entity types for. + +.PARAMETER Tables + The tables to generate entity types for. + +.PARAMETER DataAnnotations + Use attributes to configure the model (where possible). If omitted, only the fluent API is used. + +.PARAMETER Force + Overwrite existing files. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + about_EntityFrameworkCore +#> +function Scaffold-DbContext +{ + [CmdletBinding(PositionalBinding = $false)] + param( + [Parameter(Position = 0, Mandatory = $true)] + [string] $Connection, + [Parameter(Position = 1, Mandatory = $true)] + [string] $Provider, + [string] $OutputDir, + [string] $Context, + [string[]] $Schemas = @(), + [string[]] $Tables = @(), + [switch] $DataAnnotations, + [switch] $Force, + [string] $Environment, + [string] $Project, + [string] $StartupProject) + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + $params = 'dbcontext', 'scaffold', $Connection, $Provider, '--json' + + if ($OutputDir) + { + $params += '--output-dir', $OutputDir + } + + if ($Context) + { + $params += '--context', $Context + } + + $params += $Schemas | %{ '--schema', $_ } + $params += $Tables | %{ '--table', $_ } + + if ($DataAnnotations) + { + $params += '--data-annotations' + } + + if ($Force) + { + $params += '--force' + } + + $params += GetParams -Environment $Environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $result = (EF $dteProject $dteStartupProject $params) -join "`n" | ConvertFrom-Json + + $result | %{ $dteProject.ProjectItems.AddFromFile($_) | Out-Null } + $DTE.ItemOperations.OpenFile($result[0]) | Out-Null + ShowConsole +} + +# +# Script-Migration +# + +Register-TabExpansion Script-Migration @{ + From = { param($x) GetMigrations $x.Context $x.Environment $x.Project $x.StartupProject } + To = { param($x) GetMigrations $x.Context $x.Environment $x.Project $x.StartupProject } + Context = { param($x) GetContextTypes $x.Environment $x.Project $x.StartupProject } + Project = { GetProjects } + StartupProject = { GetProjects } +} + +<# +.SYNOPSIS + Generates a SQL script from migrations. + +.DESCRIPTION + Generates a SQL script from migrations. + +.PARAMETER From + The starting migration. Defaults to '0' (the initial database). + +.PARAMETER To + The ending migration. Defaults to the last migration. + +.PARAMETER Idempotent + Generate a script that can be used on a database at any migration. + +.PARAMETER Output + The file to write the result to. + +.PARAMETER Context + The DbContext to use. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + Update-Database + about_EntityFrameworkCore +#> +function Script-Migration +{ + [CmdletBinding(PositionalBinding = $false)] + param( + [Parameter(ParameterSetName = 'WithoutTo', Position = 0)] + [Parameter(ParameterSetName = 'WithTo', Position = 0, Mandatory = $true)] + [string] $From, + [Parameter(ParameterSetName = 'WithTo', Position = 1, Mandatory = $true)] + [string] $To, + [switch] $Idempotent, + [string] $Output, + [string] $Context, + [string] $Environment, + [string] $Project, + [string] $StartupProject) + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + if (!$Output) + { + $intermediatePath = GetIntermediatePath $dteProject + if (!(Split-Path $intermediatePath -IsAbsolute)) + { + $projectDir = GetProperty $dteProject.Properties 'FullPath' + $intermediatePath = Join-Path $projectDir $intermediatePath -Resolve + } + + $scriptFileName = [IO.Path]::ChangeExtension([IO.Path]::GetRandomFileName(), '.sql') + $Output = Join-Path $intermediatePath $scriptFileName + } + elseif (!(Split-Path $Output -IsAbsolute)) + { + $Output = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Output) + } + + $params = 'migrations', 'script', '--output', $Output + + if ($From) + { + $params += $From + } + + if ($To) + { + $params += $To + } + + if ($Idempotent) + { + $params += '--idempotent' + } + + $params += GetParams $Context $Environment + + EF $dteProject $dteStartupProject $params + + $DTE.ItemOperations.OpenFile($Output) | Out-Null + ShowConsole +} + +# +# Update-Database +# + +Register-TabExpansion Update-Database @{ + Migration = { param($x) GetMigrations $x.Context $x.Environment $x.Project $x.StartupProject } + Context = { param($x) GetContextTypes $x.Environment $x.Project $x.StartupProject } + Project = { GetProjects } + StartupProject = { GetProjects } +} + +<# +.SYNOPSIS + Updates the database to a specified migration. + +.DESCRIPTION + Updates the database to a specified migration. + +.PARAMETER Migration + The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + +.PARAMETER Context + The DbContext to use. + +.PARAMETER Environment + The environment to use. Defaults to "Development". + +.PARAMETER Project + The project to use. + +.PARAMETER StartupProject + The startup project to use. Defaults to the solution's startup project. + +.LINK + Script-Migration + about_EntityFrameworkCore +#> +function Update-Database +{ + [CmdletBinding(PositionalBinding = $false)] + param( + [Parameter(Position = 0)] + [string] $Migration, + [string] $Context, + [string] $Environment, + [string] $Project, + [string] $StartupProject) + + WarnIfEF6 'Update-Database' + + $dteProject = GetProject $Project + $dteStartupProject = GetStartupProject $StartupProject $dteProject + + if (IsUWP $dteStartupProject) + { + throw 'Update-Database shouldn''t be used with Universal Windows Platform apps. Instead, call ' + + 'DbContext.Database.Migrate() at runtime.' + } + + $params = 'database', 'update' + + if ($Migration) + { + $params += $Migration + } + + $params += GetParams $Context $Environment + + EF $dteProject $dteStartupProject $params +} + +# +# (Private Helpers) +# + +function GetProjects +{ + return Get-Project -All | %{ $_.ProjectName } +} + +function GetProviders($projectName) +{ + if (!$projectName) + { + $projectName = (Get-Project).ProjectName + } + + return Get-Package -ProjectName $projectName | %{ $_.Id } +} + +function GetContextTypes($environment, $projectName, $startupProjectName) +{ + $project = GetProject $projectName + $startupProject = GetStartupProject $startupProjectName $project + + $params = 'dbcontext', 'list', '--json' + $params += GetParams -Environment $environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $result = (EF $project $startupProject $params -skipBuild) -join "`n" | ConvertFrom-Json + + return $result | %{ $_.safeName } +} + +function GetMigrations($context, $environment, $projectName, $startupProjectName) +{ + $project = GetProject $projectName + $startupProject = GetStartupProject $startupProjectName $project + + $params = 'migrations', 'list', '--json' + $params += GetParams $context $environment + + # NB: -join is here to support ConvertFrom-Json on PowerShell 3.0 + $result = (EF $project $startupProject $params -skipBuild) -join "`n" | ConvertFrom-Json + + return $result | %{ $_.safeName } +} + +function WarnIfEF6 ($cmdlet) +{ + if (Get-Module 'EntityFramework') + { + Write-Warning "Both Entity Framework Core and Entity Framework 6 are installed. The Entity Framework Core tools are running. Use 'EntityFramework\$cmdlet' for Entity Framework 6." + } +} + +function GetProject($projectName) +{ + if (!$projectName) + { + return Get-Project + } + + return Get-Project $projectName +} + +function GetStartupProject($name, $fallbackProject) +{ + if ($name) + { + return Get-Project $name + } + + $startupProjectPaths = $DTE.Solution.SolutionBuild.StartupProjects + if ($startupProjectPaths) + { + if ($startupProjectPaths.Length -eq 1) + { + $startupProjectPath = $startupProjectPaths[0] + if (!(Split-Path -IsAbsolute $startupProjectPath)) + { + $solutionPath = Split-Path (GetProperty $DTE.Solution.Properties 'Path') + $startupProjectPath = Join-Path $solutionPath $startupProjectPath -Resolve + } + + $startupProject = GetSolutionProjects | ?{ + try + { + $fullName = $_.FullName + } + catch [NotImplementedException] + { + return $false + } + + if ($fullName -and $fullName.EndsWith('\')) + { + $fullName = $fullName.Substring(0, $fullName.Length - 1) + } + + return $fullName -eq $startupProjectPath + } + if ($startupProject) + { + return $startupProject + } + + Write-Warning "Unable to resolve startup project '$startupProjectPath'." + } + else + { + Write-Verbose 'More than one startup project found.' + } + } + else + { + Write-Verbose 'No startup project found.' + } + + return $fallbackProject +} + +function GetSolutionProjects() +{ + $projects = New-Object 'System.Collections.Stack' + + $DTE.Solution.Projects | %{ + $projects.Push($_) + } + + while ($projects.Count) + { + $project = $projects.Pop(); + + <# yield return #> $project + + if ($project.ProjectItems) + { + $project.ProjectItems | ?{ $_.SubProject } | %{ + $projects.Push($_.SubProject) + } + } + } +} + +function GetParams($context, $environment) +{ + $params = @() + + if ($context) + { + $params += '--context', $context + } + + if ($environment) + { + $params += '--environment', $environment + } + + return $params +} + +function ShowConsole +{ + $componentModel = Get-VSComponentModel + $powerConsoleWindow = $componentModel.GetService([NuGetConsole.IPowerConsoleWindow]) + $powerConsoleWindow.Show() +} + +function EF($project, $startupProject, $params, [switch] $skipBuild) +{ + if (IsUWP $project) + { + $outputType = GetProperty $project.Properties 'OutputType' + $outputTypeEx = GetProperty $project.Properties 'OutputTypeEx' + if ($outputType -eq 2 -and $outputTypeEx -eq 3) + { + throw "Project '$($project.ProjectName)' is a Windows Runtime component. The Entity Framework Core " + + 'Package Manager Console Tools don''t support this type of project.' + } + } + + if (IsXproj $startupProject) + { + throw "Startup project '$($startupProject.ProjectName)' is an ASP.NET Core or .NET Core project for Visual " + + 'Studio 2015. This version of the Entity Framework Core Package Manager Console Tools doesn''t support ' + + 'these types of projects.' + } + if (IsUWP $startupProject) + { + $useDotNetNative = GetProperty $startupProject.ConfigurationManager.ActiveConfiguration.Properties 'ProjectN.UseDotNetNativeToolchain' + if ($useDotNetNative -eq 'True') + { + throw "Startup project '$($startupProject.ProjectName)' compiles with the .NET Native tool chan. Uncheck " + + 'this option in the project settings or use a different configuration and try again.' + } + + $outputType = GetProperty $startupProject.Properties 'OutputType' + if ($outputType -eq 2) + { + $outputTypeEx = GetProperty $startupProject.Properties 'OutputTypeEx' + if ($outputTypeEx -eq 2) + { + throw "Startup project '$($startupProject.ProjectName)' is a class library. Select a Universal " + + 'Windows Platform app as your startup project and try again.' + } + if ($outputTypeEx -eq 3) + { + throw "Startup project '$($startupProject.ProjectName)' is a Windows Runtime component. The Entity " + + 'Framework Core Package Manager Console Tools don''t support this type of project.' + } + } + } + + Write-Verbose "Using project '$($project.ProjectName)'." + Write-Verbose "Using startup project '$($startupProject.ProjectName)'." + + if (!$skipBuild) + { + Write-Verbose 'Build started...' + + # TODO: Only build startup project. Don't use BuildProject, you can't specify platform + $solutionBuild = $DTE.Solution.SolutionBuild + $solutionBuild.Build(<# WaitForBuildToFinish: #> $true) + if ($solutionBuild.LastBuildInfo) + { + throw 'Build failed.' + } + + Write-Verbose 'Build succeeded.' + } + + $startupProjectDir = GetProperty $startupProject.Properties 'FullPath' + $outputPath = GetProperty $startupProject.ConfigurationManager.ActiveConfiguration.Properties 'OutputPath' + $targetDir = Join-Path $startupProjectDir $outputPath -Resolve + $startupTargetFileName = GetOutputFileName $startupProject + $startupTargetPath = Join-Path $targetDir $startupTargetFileName + $targetFrameworkMoniker = GetProperty $startupProject.Properties 'TargetFrameworkMoniker' + $frameworkName = New-Object 'System.Runtime.Versioning.FrameworkName' $targetFrameworkMoniker + $targetFramework = $frameworkName.Identifier + + if ($targetFramework -in '.NETFramework', '.NETCore') + { + $platformTarget = GetPlatformTarget $startupProject + if ($platformTarget -eq 'x86') + { + $exePath = Join-Path $PSScriptRoot 'net451\ef.x86.exe' + } + elseif ($platformTarget -in 'AnyCPU', 'x64') + { + $exePath = Join-Path $PSScriptRoot 'net451\ef.exe' + } + else + { + throw "Startup project '$($startupProject.ProjectName)' has an active platform of '$platformTarget'. Select " + + 'a different platform and try again.' + } + } + elseif ($targetFramework -in '.NETCoreApp', '.NETStandard') + { + if ($targetFramework -eq '.NETStandard') + { + Write-Warning ("Startup project '$($startupProject.ProjectName)' targets framework '.NETStandard'. This " + + "framework is not intended for execution and may fail to resolve runtime dependencies. If so, select " + + "a different startup project and try again.") + } + + $exePath = (Get-Command 'dotnet').Path + + $startupTargetName = GetProperty $startupProject.Properties 'AssemblyName' + $depsFile = Join-Path $targetDir ($startupTargetName + '.deps.json') + $projectAssetsFile = GetCsproj2Property $startupProject 'ProjectAssetsFile' + $runtimeConfig = Join-Path $targetDir ($startupTargetName + '.runtimeconfig.json') + $efPath = Join-Path $PSScriptRoot 'netcoreapp1.0\ef.dll' + + $dotnetParams = 'exec', '--depsfile', $depsFile + + if ($projectAssetsFile) + { + # NB: -Raw is here to support ConvertFrom-Json on PowerShell 3.0 + $projectAssets = Get-Content $projectAssetsFile -Raw | ConvertFrom-Json + $projectAssets.packageFolders.psobject.Properties.Name | %{ + $dotnetParams += '--additionalprobingpath', $_.TrimEnd('\') + } + } + + if (Test-Path $runtimeConfig) + { + $dotnetParams += '--runtimeconfig', $runtimeConfig + } + + $dotnetParams += $efPath + + $params = $dotnetParams + $params + } + else + { + throw "Startup project '$($startupProject.ProjectName)' targets framework '$targetFramework'. " + + 'The Entity Framework Core Package Manager Console Tools don''t support this framework.' + } + + $projectDir = GetProperty $project.Properties 'FullPath' + $targetFileName = GetOutputFileName $project + $targetPath = Join-Path $targetDir $targetFileName + $rootNamespace = GetProperty $project.Properties 'RootNamespace' + + if (IsWeb $startupProject) + { + $dataDir = Join-Path $startupProjectDir 'App_Data' + } + else + { + $dataDir = $targetDir + } + + $params += '--verbose', + '--no-color', + '--prefix-output', + '--assembly', $targetPath, + '--startup-assembly', $startupTargetPath, + '--project-dir', $projectDir, + '--content-root', $startupProjectDir, + '--data-dir', $dataDir + + if (IsUWP $startupProject) + { + $params += '--no-appdomain' + } + + if ($rootNamespace) + { + $params += '--root-namespace', $rootNamespace + } + + $arguments = ToArguments $params + $startInfo = New-Object 'System.Diagnostics.ProcessStartInfo' -Property @{ + FileName = $exePath; + Arguments = $arguments; + UseShellExecute = $false; + CreateNoWindow = $true; + RedirectStandardOutput = $true; + StandardOutputEncoding = [Text.Encoding]::UTF8; + } + + Write-Verbose "$exePath $arguments" + + $process = [Diagnostics.Process]::Start($startInfo) + + while ($line = $process.StandardOutput.ReadLine()) + { + $level = $null + $text = $null + + $parts = $line.Split(':', 2) + if ($parts.Length -eq 2) + { + $level = $parts[0] + $text = $parts[1].Substring(8 - $level.Length) + } + + switch ($level) + { + 'error' { throw $text } + 'warn' { Write-Warning $text } + 'info' { Write-Host $text } + 'data' { Write-Output $text } + 'verbose' { Write-Verbose $text } + default { Write-Host $line } + } + } + + $process.WaitForExit() +} + +function IsXproj($project) +{ + return $project.Kind -eq '{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}' +} + +function IsCsproj2($project) +{ + return $project.Kind -eq '{9A19103F-16F7-4668-BE54-9A1E7A4F7556}' +} + +function IsWeb($project) +{ + $types = GetProjectTypes $project + + return $types -contains '{349C5851-65DF-11DA-9384-00065B846F21}' +} + +function IsUWP($project) +{ + $types = GetProjectTypes $project + + return $types -contains '{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A}' +} + +function GetIntermediatePath($project) +{ + # TODO: Remove when dotnet/roslyn-project-system#665 is fixed + if (IsCsproj2 $project) + { + return GetCsproj2Property $project 'IntermediateOutputPath' + } + + return GetProperty $project.ConfigurationManager.ActiveConfiguration.Properties 'IntermediatePath' +} + +function GetPlatformTarget($project) +{ + # TODO: Remove when dotnet/roslyn-project-system#669 is fixed + if (IsCsproj2 $project) + { + $platformTarget = GetCsproj2Property $project 'PlatformTarget' + if ($platformTarget) + { + return $platformTarget + } + + return GetCsproj2Property $project 'Platform' + } + + return GetProperty $project.ConfigurationManager.ActiveConfiguration.Properties 'PlatformTarget' +} + +function GetOutputFileName($project) +{ + # TODO: Remove when dotnet/roslyn-project-system#667 is fixed + if (IsCsproj2 $project) + { + return GetCsproj2Property $project 'TargetFileName' + } + + return GetProperty $project.Properties 'OutputFileName' +} + +function GetProjectTypes($project) +{ + $solution = Get-VSService 'Microsoft.VisualStudio.Shell.Interop.SVsSolution' 'Microsoft.VisualStudio.Shell.Interop.IVsSolution' + $hierarchy = $null + $hr = $solution.GetProjectOfUniqueName($project.UniqueName, [ref] $hierarchy) + [Runtime.InteropServices.Marshal]::ThrowExceptionForHR($hr) + + $aggregatableProject = Get-Interface $hierarchy 'Microsoft.VisualStudio.Shell.Interop.IVsAggregatableProject' + if (!$aggregatableProject) + { + return $project.Kind + } + + $projectTypeGuidsString = $null + $hr = $aggregatableProject.GetAggregateProjectTypeGuids([ref] $projectTypeGuidsString) + [Runtime.InteropServices.Marshal]::ThrowExceptionForHR($hr) + + return $projectTypeGuidsString.Split(';') +} + +function GetProperty($properties, $propertyName) +{ + try + { + return $properties.Item($propertyName).Value + } + catch + { + return $null + } +} + +function GetCsproj2Property($project, $propertyName) +{ + $browseObjectContext = Get-Interface $project 'Microsoft.VisualStudio.ProjectSystem.Properties.IVsBrowseObjectContext' + $unconfiguredProject = $browseObjectContext.UnconfiguredProject + $configuredProject = $unconfiguredProject.GetSuggestedConfiguredProjectAsync().Result + $properties = $configuredProject.Services.ProjectPropertiesProvider.GetCommonProperties() + + return $properties.GetEvaluatedPropertyValueAsync($propertyName).Result +} + +function GetProjectItem($project, $path) +{ + $fullPath = GetProperty $project.Properties 'FullPath' + + if (Split-Path $path -IsAbsolute) + { + $path = $path.Substring($fullPath.Length) + } + + $itemDirectory = (Split-Path $path -Parent) + + $projectItems = $project.ProjectItems + if ($itemDirectory) + { + $directories = $itemDirectory.Split('\') + $directories | %{ + if ($projectItems) + { + $projectItems = $projectItems.Item($_).ProjectItems + } + } + } + + if (!$projectItems) + { + return $null + } + + $itemName = Split-Path $path -Leaf + + try + { + return $projectItems.Item($itemName) + } + catch [Exception] + { + } + + return $null +} + +function ToArguments($params) +{ + $arguments = '' + for ($i = 0; $i -lt $params.Length; $i++) + { + if ($i) + { + $arguments += " " + } + + if (!$params[$i].Contains(' ')) + { + $arguments += $params[$i] + + continue + } + + $arguments += '"' + + $pendingBackslashs = 0 + for ($j = 0; $j -lt $params[$i].Length; $j++) + { + switch ($params[$i][$j]) + { + '"' + { + if ($pendingBackslashs) + { + $arguments += '\' * $pendingBackslashs * 2 + $pendingBackslashs = 0 + } + $arguments += '\"' + } + + '\' + { + $pendingBackslashs++ + } + + default + { + if ($pendingBackslashs) + { + if ($pendingBackslashs -eq 1) + { + $arguments += '\' + } + else + { + $arguments += '\' * $pendingBackslashs * 2 + } + + $pendingBackslashs = 0 + } + + $arguments += $params[$i][$j] + } + } + } + + if ($pendingBackslashs) + { + $arguments += '\' * $pendingBackslashs * 2 + } + + $arguments += '"' + } + + return $arguments +} diff --git a/src/EFCore.Tools/tools/about_EntityFrameworkCore.help.txt b/src/EFCore.Tools/tools/about_EntityFrameworkCore.help.txt new file mode 100644 index 0000000000..c167ed5abf --- /dev/null +++ b/src/EFCore.Tools/tools/about_EntityFrameworkCore.help.txt @@ -0,0 +1,41 @@ + + _/\__ + ---==/ \\ + ___ ___ |. \|\ + | __|| __| | ) \\\ + | _| | _| \_/ | //|\\ + |___||_| / \\\/\\ + +TOPIC + about_EntityFrameworkCore + +SHORT DESCRIPTION + Provides information about the Entity Framework Core Package Manager Console Tools. + +LONG DESCRIPTION + This topic describes the Entity Framework Core Package Manager Console Tools. See https://docs.efproject.net for + information on Entity Framework Core. + + The following Entity Framework Core commands are available. + + Cmdlet Description + -------------------------- --------------------------------------------------- + Add-Migration Adds a new migration. + + Drop-Database Drops the database. + + Remove-Migration Removes the last migration. + + Scaffold-DbContext Scaffolds a DbContext and entity types for a database. + + Script-Migration Generates a SQL script from migrations. + + Update-Database Updates the database to a specified migration. + +SEE ALSO + Add-Migration + Drop-Database + Remove-Migration + Scaffold-DbContext + Script-Migration + Update-Database diff --git a/src/EFCore.Tools/tools/init.ps1 b/src/EFCore.Tools/tools/init.ps1 new file mode 100644 index 0000000000..34b5cb4dc8 --- /dev/null +++ b/src/EFCore.Tools/tools/init.ps1 @@ -0,0 +1,24 @@ +param($installPath, $toolsPath, $package, $project) + +if ($PSVersionTable.PSVersion.Major -lt 3) +{ + # This section needs to support PS2 syntax + # Use $toolsPath because PS2 does not support $PSScriptRoot + $env:PSModulePath = $env:PSModulePath + ';$toolsPath' + + # Import a "dummy" module that contains matching functions that throw on PS2 + Import-Module (Join-Path $toolsPath 'EntityFrameworkCore.PowerShell2.psd1') -DisableNameChecking + + throw "PowerShell version $($PSVersionTable.PSVersion) is not supported. Please upgrade PowerShell to 3.0 or " + + 'greater and restart Visual Studio.' +} +else +{ + if (Get-Module 'EntityFrameworkCore') + { + Remove-Module 'EntityFrameworkCore' + } + + Import-Module (Join-Path $PSScriptRoot 'EntityFrameworkCore.psd1') -DisableNameChecking +} + diff --git a/src/EFCore.Tools/tools/install.ps1 b/src/EFCore.Tools/tools/install.ps1 new file mode 100644 index 0000000000..d260b151f7 --- /dev/null +++ b/src/EFCore.Tools/tools/install.ps1 @@ -0,0 +1,5 @@ +param($installPath, $toolsPath, $package, $project) + +Write-Host +Write-Host 'Type ''get-help EntityFrameworkCore'' to see all available Entity Framework Core commands.' +Write-Host diff --git a/src/EFCore.Tools/tools/net451/ef.exe.config b/src/EFCore.Tools/tools/net451/ef.exe.config new file mode 100644 index 0000000000..bbc7a1a4e6 --- /dev/null +++ b/src/EFCore.Tools/tools/net451/ef.exe.config @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/EFCore.Tools/tools/net451/ef.x86.exe.config b/src/EFCore.Tools/tools/net451/ef.x86.exe.config new file mode 100644 index 0000000000..bbc7a1a4e6 --- /dev/null +++ b/src/EFCore.Tools/tools/net451/ef.x86.exe.config @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dotnet-ef/Commands/ProjectCommandBase.cs b/src/dotnet-ef/Commands/ProjectCommandBase.cs new file mode 100644 index 0000000000..ab679d23ba --- /dev/null +++ b/src/dotnet-ef/Commands/ProjectCommandBase.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class ProjectCommandBase : EnvironmentCommandBase + { + public override void Configure(CommandLineApplication command) + { + new ProjectOptions().Configure(command); + + base.Configure(command); + } + } +} diff --git a/src/dotnet-ef/Exe.cs b/src/dotnet-ef/Exe.cs new file mode 100644 index 0000000000..a94d58bfa3 --- /dev/null +++ b/src/dotnet-ef/Exe.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class Exe + { + public static int Run(string executable, IReadOnlyList args) + { + var arguments = ToArguments(args); + + Reporter.WriteVerbose(executable + " " + arguments); + + var build = Process.Start( + new ProcessStartInfo + { + FileName = executable, + Arguments = arguments, + UseShellExecute = false + }); + build.WaitForExit(); + + return build.ExitCode; + } + + private static string ToArguments(IReadOnlyList args) + { + var builder = new StringBuilder(); + for (var i = 0; i < args.Count; i++) + { + if (i != 0) + { + builder.Append(" "); + } + + if (args[i].IndexOf(' ') == -1) + { + builder.Append(args[i]); + + continue; + } + + builder.Append("\""); + + var pendingBackslashs = 0; + for (var j = 0; j < args[i].Length; j++) + { + switch (args[i][j]) + { + case '\"': + if (pendingBackslashs != 0) + { + builder.Append('\\', pendingBackslashs * 2); + pendingBackslashs = 0; + } + builder.Append("\\\""); + break; + + case '\\': + pendingBackslashs++; + break; + + default: + if (pendingBackslashs != 0) + { + if (pendingBackslashs == 1) + { + builder.Append("\\"); + } + else + { + builder.Append('\\', pendingBackslashs * 2); + } + + pendingBackslashs = 0; + } + + builder.Append(args[i][j]); + break; + } + } + + if (pendingBackslashs != 0) + { + builder.Append('\\', pendingBackslashs * 2); + } + + builder.Append("\""); + } + + return builder.ToString(); + } + } +} diff --git a/src/dotnet-ef/Program.cs b/src/dotnet-ef/Program.cs new file mode 100644 index 0000000000..d293e23132 --- /dev/null +++ b/src/dotnet-ef/Program.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.DotNet.Cli.CommandLine; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class Program + { + private static int Main(string[] args) + { + var app = new CommandLineApplication(throwOnUnexpectedArg: false) + { + Name = "dotnet ef" + }; + + new RootCommand().Configure(app); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { + if (ex is CommandException || ex is CommandParsingException) + { + Reporter.WriteVerbose(ex.ToString()); + } + else + { + Reporter.WriteInformation(ex.ToString()); + } + + Reporter.WriteError(ex.Message); + + return 1; + } + } + } +} diff --git a/src/dotnet-ef/Project.cs b/src/dotnet-ef/Project.cs new file mode 100644 index 0000000000..d57f15ef6e --- /dev/null +++ b/src/dotnet-ef/Project.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class Project + { + private readonly string _file; + private readonly string _framework; + private readonly string _configuration; + + public Project(string file, string framework, string configuration) + { + Debug.Assert(!string.IsNullOrEmpty(file), "file is null or empty."); + + _file = file; + _framework = framework; + _configuration = configuration; + ProjectName = Path.GetFileName(file); + } + + public string ProjectName { get; } + + public string AssemblyName { get; set; } + public string OutputPath { get; set; } + public string PlatformTarget { get; set; } + public string ProjectAssetsFile { get; set; } + public string ProjectDir { get; set; } + public string RootNamespace { get; set; } + public string TargetFileName { get; set; } + public string TargetFrameworkMoniker { get; set; } + + public static Project FromFile( + string file, + string buildExtensionsDir, + string framework = null, + string configuration = null) + { + Debug.Assert(!string.IsNullOrEmpty(file), "file is null or empty."); + + if (buildExtensionsDir == null) + { + buildExtensionsDir = Path.Combine(Path.GetDirectoryName(file), "obj"); + } + + var efTargetsPath = Path.Combine( + buildExtensionsDir, + Path.GetFileName(file) + ".EntityFrameworkCore.targets"); + if (!File.Exists(efTargetsPath)) + { + Reporter.WriteVerbose(Resources.WritingFile(efTargetsPath)); + + using (var input = typeof(Resources).GetTypeInfo().Assembly.GetManifestResourceStream( + "Microsoft.EntityFrameworkCore.Tools.Resources.EntityFrameworkCore.targets")) + using (var output = File.OpenWrite(efTargetsPath)) + { + input.CopyTo(output); + } + } + + IDictionary metadata; + var metadataFile = Path.GetTempFileName(); + try + { + var propertyArg = "/property:EFProjectMetadataFile=" + metadataFile; + if (configuration != null) + { + propertyArg += ";TargetFramework=" + framework; + } + if (configuration != null) + { + propertyArg += ";Configuration=" + configuration; + } + + var args = new List + { + "msbuild", + "/target:GetEFProjectMetadata", + propertyArg, + "/verbosity:quiet", + "/nologo" + }; + + if (file != null) + { + args.Add(file); + } + + var exitCode = Exe.Run("dotnet", args); + if (exitCode != 0) + { + throw new CommandException(Resources.GetMetadataFailed); + } + + metadata = File.ReadLines(metadataFile).Select(l => l.Split(new[] { ':' }, 2)) + .ToDictionary(s => s[0], s => s[1].TrimStart()); + } + finally + { + File.Delete(metadataFile); + } + + var platformTarget = metadata["PlatformTarget"]; + if (platformTarget.Length == 0) + { + platformTarget = metadata["Platform"]; + } + + return new Project(file, framework, configuration) + { + AssemblyName = metadata["AssemblyName"], + OutputPath = metadata["OutputPath"], + PlatformTarget = platformTarget, + ProjectAssetsFile = metadata["ProjectAssetsFile"], + ProjectDir = metadata["ProjectDir"], + RootNamespace = metadata["RootNamespace"], + TargetFileName = metadata["TargetFileName"], + TargetFrameworkMoniker = metadata["TargetFrameworkMoniker"] + }; + } + + public void Build() + { + var args = new List(); + + args.Add("build"); + + if (_file != null) + { + args.Add(_file); + } + + // TODO: Only build for the first framework when unspecified + if (_framework != null) + { + args.Add("--framework"); + args.Add(_framework); + } + + if (_configuration != null) + { + args.Add("--configuration"); + args.Add(_configuration); + } + + args.Add("/verbosity:quiet"); + args.Add("/nologo"); + + + var exitCode = Exe.Run("dotnet", args); + if (exitCode != 0) + { + throw new CommandException(Resources.BuildFailed); + } + } + } +} diff --git a/src/dotnet-ef/ProjectOptions.cs b/src/dotnet-ef/ProjectOptions.cs new file mode 100644 index 0000000000..2ab970047c --- /dev/null +++ b/src/dotnet-ef/ProjectOptions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class ProjectOptions + { + private CommandOption _project; + private CommandOption _startupProject; + private CommandOption _framework; + private CommandOption _configuration; + private CommandOption _msbuildprojectextensionspath; + + public CommandOption Project + => _project; + + public CommandOption StartupProject + => _startupProject; + + public CommandOption Framework + => _framework; + + public CommandOption Configuration + => _configuration; + + public CommandOption MSBuildProjectExtensionsPath + => _msbuildprojectextensionspath; + + public void Configure(CommandLineApplication command) + { + _project = command.Option("-p|--project ", Resources.ProjectDescription); + _startupProject = command.Option("-s|--startup-project ", Resources.StartupProjectDescription); + _framework = command.Option("--framework ", Resources.FrameworkDescription); + _configuration = command.Option("--configuration ", Resources.ConfigurationDescription); + _msbuildprojectextensionspath = command.Option("--msbuildprojectextensionspath ", Resources.ProjectExtensionsDescription); + } + } +} diff --git a/src/dotnet-ef/Properties/AssemblyInfo.cs b/src/dotnet-ef/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b251f4820c --- /dev/null +++ b/src/dotnet-ef/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo( + "dotnet-ef.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/dotnet-ef/Properties/Resources.Designer.cs b/src/dotnet-ef/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..ff9f7d09c1 --- /dev/null +++ b/src/dotnet-ef/Properties/Resources.Designer.cs @@ -0,0 +1,379 @@ +// + +using System.Reflection; +using System.Resources; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Tools.Properties +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.EntityFrameworkCore.Tools.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Build failed. + /// + public static string BuildFailed + => GetString("BuildFailed"); + + /// + /// The configuration to use. + /// + public static string ConfigurationDescription + => GetString("ConfigurationDescription"); + + /// + /// The connection string to the database. + /// + public static string ConnectionDescription + => GetString("ConnectionDescription"); + + /// + /// The DbContext to use. + /// + public static string ContextDescription + => GetString("ContextDescription"); + + /// + /// The name of the DbContext. + /// + public static string ContextNameDescription + => GetString("ContextNameDescription"); + + /// + /// Use attributes to configure the model (where possible). If omitted, only the fluent API is used. + /// + public static string DataAnnotationsDescription + => GetString("DataAnnotationsDescription"); + + /// + /// Commands to manage the database. + /// + public static string DatabaseDescription + => GetString("DatabaseDescription"); + + /// + /// Drops the database. + /// + public static string DatabaseDropDescription + => GetString("DatabaseDropDescription"); + + /// + /// Show which database would be dropped, but don't drop it. + /// + public static string DatabaseDropDryRunDescription + => GetString("DatabaseDropDryRunDescription"); + + /// + /// Don't confirm. + /// + public static string DatabaseDropForceDescription + => GetString("DatabaseDropForceDescription"); + + /// + /// Updates the database to a specified migration. + /// + public static string DatabaseUpdateDescription + => GetString("DatabaseUpdateDescription"); + + /// + /// Commands to manage DbContext types. + /// + public static string DbContextDescription + => GetString("DbContextDescription"); + + /// + /// Gets information about a DbContext type. + /// + public static string DbContextInfoDescription + => GetString("DbContextInfoDescription"); + + /// + /// Lists available DbContext types. + /// + public static string DbContextListDescription + => GetString("DbContextListDescription"); + + /// + /// Scaffolds a DbContext and entity types for a database. + /// + public static string DbContextScaffoldDescription + => GetString("DbContextScaffoldDescription"); + + /// + /// Overwrite existing files. + /// + public static string DbContextScaffoldForceDescription + => GetString("DbContextScaffoldForceDescription"); + + /// + /// Entity Framework Core .NET Command Line Tools + /// + public static string DotnetEfFullName + => GetString("DotnetEfFullName"); + + /// + /// Entity Framework Core Command Line Tools + /// + public static string EFFullName + => GetString("EFFullName"); + + /// + /// The environment to use. Defaults to "Development". + /// + public static string EnvironmentDescription + => GetString("EnvironmentDescription"); + + /// + /// The target framework. + /// + public static string FrameworkDescription + => GetString("FrameworkDescription"); + + /// + /// Unable to retrieve project metadata. Ensure it's an MSBuild-based .NET Core project. If you're using custom BaseIntermediateOutputPath or MSBuildProjectExtensionsPath values, Use the --msbuildprojectextensionspath option. + /// + public static string GetMetadataFailed + => GetString("GetMetadataFailed"); + + /// + /// Generate a script that can be used on a database at any migration. + /// + public static string IdempotentDescription + => GetString("IdempotentDescription"); + + /// + /// Show JSON output. + /// + public static string JsonDescription + => GetString("JsonDescription"); + + /// + /// The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + /// + public static string MigrationDescription + => GetString("MigrationDescription"); + + /// + /// The starting migration. Defaults to '0' (the initial database). + /// + public static string MigrationFromDescription + => GetString("MigrationFromDescription"); + + /// + /// The name of the migration. + /// + public static string MigrationNameDescription + => GetString("MigrationNameDescription"); + + /// + /// Adds a new migration. + /// + public static string MigrationsAddDescription + => GetString("MigrationsAddDescription"); + + /// + /// Commands to manage migrations. + /// + public static string MigrationsDescription + => GetString("MigrationsDescription"); + + /// + /// Lists available migrations. + /// + public static string MigrationsListDescription + => GetString("MigrationsListDescription"); + + /// + /// The directory (and sub-namespace) to use. Paths are relative to the project directory. Defaults to "Migrations". + /// + public static string MigrationsOutputDirDescription + => GetString("MigrationsOutputDirDescription"); + + /// + /// Removes the last migration. + /// + public static string MigrationsRemoveDescription + => GetString("MigrationsRemoveDescription"); + + /// + /// Don't check to see if the migration has been applied to the database. + /// + public static string MigrationsRemoveForceDescription + => GetString("MigrationsRemoveForceDescription"); + + /// + /// Generates a SQL script from migrations. + /// + public static string MigrationsScriptDescription + => GetString("MigrationsScriptDescription"); + + /// + /// The ending migration. Defaults to the last migration. + /// + public static string MigrationToDescription + => GetString("MigrationToDescription"); + + /// + /// More than one project was found in the current working directory. Use the --project option. + /// + public static string MultipleProjects + => GetString("MultipleProjects"); + + /// + /// More than one project was found in directory '{projectDir}'. Specify one using its file name. + /// + public static string MultipleProjectsInDirectory([CanBeNull] object projectDir) + => string.Format( + GetString("MultipleProjectsInDirectory", nameof(projectDir)), + projectDir); + + /// + /// More than one project was found in the current working directory. Use the --startup-project option. + /// + public static string MultipleStartupProjects + => GetString("MultipleStartupProjects"); + + /// + /// Startup project '{startupProject}' targets framework '.NETStandard'. This framework is not intended for execution and may fail to resolve runtime dependencies. If so, specify a different project using the --startup-project option and try again. + /// + public static string NETStandardStartupProject([CanBeNull] object startupProject) + => string.Format( + GetString("NETStandardStartupProject", nameof(startupProject)), + startupProject); + + /// + /// Don't colorize output. + /// + public static string NoColorDescription + => GetString("NoColorDescription"); + + /// + /// No project was found. Change the current working directory or use the --project option. + /// + public static string NoProject + => GetString("NoProject"); + + /// + /// No project was found in directory '{projectDir}'. + /// + public static string NoProjectInDirectory([CanBeNull] object projectDir) + => string.Format( + GetString("NoProjectInDirectory", nameof(projectDir)), + projectDir); + + /// + /// No project was found. Change the current working directory or use the --startup-project option. + /// + public static string NoStartupProject + => GetString("NoStartupProject"); + + /// + /// The file to write the result to. + /// + public static string OutputDescription + => GetString("OutputDescription"); + + /// + /// The directory to put files in. Paths are relative to the project directory. + /// + public static string OutputDirDescription + => GetString("OutputDirDescription"); + + /// + /// Prefix output with level. + /// + public static string PrefixDescription + => GetString("PrefixDescription"); + + /// + /// The project to use. + /// + public static string ProjectDescription + => GetString("ProjectDescription"); + + /// + /// The MSBuild project extensions path. Defaults to "obj". + /// + public static string ProjectExtensionsDescription + => GetString("ProjectExtensionsDescription"); + + /// + /// The provider to use. (E.g. Microsoft.EntityFrameworkCore.SqlServer) + /// + public static string ProviderDescription + => GetString("ProviderDescription"); + + /// + /// The schemas of tables to generate entity types for. + /// + public static string SchemasDescription + => GetString("SchemasDescription"); + + /// + /// The startup project to use. + /// + public static string StartupProjectDescription + => GetString("StartupProjectDescription"); + + /// + /// The tables to generate entity types for. + /// + public static string TablesDescription + => GetString("TablesDescription"); + + /// + /// Startup project '{startupProject}' targets framework '{targetFramework}'. The Entity Framework Core .NET Command Line Tools don't support this framework. + /// + public static string UnsupportedFramework([CanBeNull] object startupProject, [CanBeNull] object targetFramework) + => string.Format( + GetString("UnsupportedFramework", nameof(startupProject), nameof(targetFramework)), + startupProject, targetFramework); + + /// + /// Using project '{project}'. + /// + public static string UsingProject([CanBeNull] object project) + => string.Format( + GetString("UsingProject", nameof(project)), + project); + + /// + /// Using startup project '{startupProject}'. + /// + public static string UsingStartupProject([CanBeNull] object startupProject) + => string.Format( + GetString("UsingStartupProject", nameof(startupProject)), + startupProject); + + /// + /// Show verbose output. + /// + public static string VerboseDescription + => GetString("VerboseDescription"); + + /// + /// Writing '{file}'... + /// + public static string WritingFile([CanBeNull] object file) + => string.Format( + GetString("WritingFile", nameof(file)), + file); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} diff --git a/src/dotnet-ef/Properties/Resources.Designer.tt b/src/dotnet-ef/Properties/Resources.Designer.tt new file mode 100644 index 0000000000..2f70174a83 --- /dev/null +++ b/src/dotnet-ef/Properties/Resources.Designer.tt @@ -0,0 +1,5 @@ +<# + Session["ResourceFile"] = "Resources.resx"; + Session["AccessModifier"] = "internal"; +#> +<#@ include file="..\..\..\tools\Resources.tt" #> \ No newline at end of file diff --git a/src/dotnet-ef/Properties/Resources.resx b/src/dotnet-ef/Properties/Resources.resx new file mode 100644 index 0000000000..f52aa5f042 --- /dev/null +++ b/src/dotnet-ef/Properties/Resources.resx @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Build failed. + + + The configuration to use. + + + The connection string to the database. + + + The DbContext to use. + + + The name of the DbContext. + + + Use attributes to configure the model (where possible). If omitted, only the fluent API is used. + + + Commands to manage the database. + + + Drops the database. + + + Show which database would be dropped, but don't drop it. + + + Don't confirm. + + + Updates the database to a specified migration. + + + Commands to manage DbContext types. + + + Gets information about a DbContext type. + + + Lists available DbContext types. + + + Scaffolds a DbContext and entity types for a database. + + + Overwrite existing files. + + + Entity Framework Core .NET Command Line Tools + + + Entity Framework Core Command Line Tools + + + The environment to use. Defaults to "Development". + + + The target framework. + + + Unable to retrieve project metadata. Ensure it's an MSBuild-based .NET Core project. If you're using custom BaseIntermediateOutputPath or MSBuildProjectExtensionsPath values, Use the --msbuildprojectextensionspath option. + + + Generate a script that can be used on a database at any migration. + + + Show JSON output. + + + The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + + + The starting migration. Defaults to '0' (the initial database). + + + The name of the migration. + + + Adds a new migration. + + + Commands to manage migrations. + + + Lists available migrations. + + + The directory (and sub-namespace) to use. Paths are relative to the project directory. Defaults to "Migrations". + + + Removes the last migration. + + + Don't check to see if the migration has been applied to the database. + + + Generates a SQL script from migrations. + + + The ending migration. Defaults to the last migration. + + + More than one project was found in the current working directory. Use the --project option. + + + More than one project was found in directory '{projectDir}'. Specify one using its file name. + + + More than one project was found in the current working directory. Use the --startup-project option. + + + Startup project '{startupProject}' targets framework '.NETStandard'. This framework is not intended for execution and may fail to resolve runtime dependencies. If so, specify a different project using the --startup-project option and try again. + + + Don't colorize output. + + + No project was found. Change the current working directory or use the --project option. + + + No project was found in directory '{projectDir}'. + + + No project was found. Change the current working directory or use the --startup-project option. + + + The file to write the result to. + + + The directory to put files in. Paths are relative to the project directory. + + + Prefix output with level. + + + The project to use. + + + The MSBuild project extensions path. Defaults to "obj". + + + The provider to use. (E.g. Microsoft.EntityFrameworkCore.SqlServer) + + + The schemas of tables to generate entity types for. + + + The startup project to use. + + + The tables to generate entity types for. + + + Startup project '{startupProject}' targets framework '{targetFramework}'. The Entity Framework Core .NET Command Line Tools don't support this framework. + + + Using project '{project}'. + + + Using startup project '{startupProject}'. + + + Show verbose output. + + + Writing '{file}'... + + \ No newline at end of file diff --git a/src/dotnet-ef/Resources/EntityFrameworkCore.targets b/src/dotnet-ef/Resources/EntityFrameworkCore.targets new file mode 100644 index 0000000000..bfd937118d --- /dev/null +++ b/src/dotnet-ef/Resources/EntityFrameworkCore.targets @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/dotnet-ef/RootCommand.cs b/src/dotnet-ef/RootCommand.cs new file mode 100644 index 0000000000..b2e6da722d --- /dev/null +++ b/src/dotnet-ef/RootCommand.cs @@ -0,0 +1,260 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Commands; +using Microsoft.EntityFrameworkCore.Tools.Properties; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using EFCommand = Microsoft.EntityFrameworkCore.Tools.Commands.RootCommand; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class RootCommand : CommandBase + { + private CommandLineApplication _command; + private CommandOption _project; + private CommandOption _startupProject; + private CommandOption _framework; + private CommandOption _configuration; + private CommandOption _msbuildprojectextensionspath; + private CommandOption _help; + private IList _args; + + public override void Configure(CommandLineApplication command) + { + command.FullName = Resources.DotnetEfFullName; + + var options = new ProjectOptions(); + options.Configure(command); + + _project = options.Project; + _startupProject = options.StartupProject; + _framework = options.Framework; + _configuration = options.Configuration; + _msbuildprojectextensionspath = options.MSBuildProjectExtensionsPath; + + command.VersionOption("--version", GetVersion); + _help = command.Option("-h|--help", description: null); + + _args = command.RemainingArguments; + + base.Configure(command); + + _command = command; + } + + protected override int Execute() + { + var commands = _args.TakeWhile(a => a[0] != '-').ToList(); + if (_help.HasValue() || ShouldHelp(commands)) + { + return ShowHelp(_help.HasValue(), commands); + } + + var projectFile = FindProjects( + _project.Value(), + Resources.NoProject, + Resources.MultipleProjects); + Reporter.WriteVerbose(Resources.UsingProject(projectFile)); + + var starupProjectFile = FindProjects( + _startupProject.Value(), + Resources.NoStartupProject, + Resources.MultipleStartupProjects); + Reporter.WriteVerbose(Resources.UsingStartupProject(starupProjectFile)); + + var project = Project.FromFile(projectFile, _msbuildprojectextensionspath.Value()); + var startupProject = Project.FromFile( + starupProjectFile, + _msbuildprojectextensionspath.Value(), + _framework.Value(), + _configuration.Value()); + + startupProject.Build(); + + string executable; + var args = new List(); + + var toolsPath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(typeof(Program).GetTypeInfo().Assembly.Location), + "..", + "..", + "tools")); + + var targetDir = Path.GetFullPath(Path.Combine(startupProject.ProjectDir, startupProject.OutputPath)); + var targetPath = Path.Combine(targetDir, project.TargetFileName); + var startupTargetPath = Path.Combine(targetDir, startupProject.TargetFileName); + var depsFile = Path.Combine( + targetDir, + startupProject.AssemblyName + ".deps.json"); + var runtimeConfig = Path.Combine( + targetDir, + startupProject.AssemblyName + ".runtimeconfig.json"); + var projectAssetsFile = startupProject.ProjectAssetsFile; + + var targetFramework = new FrameworkName(startupProject.TargetFrameworkMoniker); + if (targetFramework.Identifier == ".NETFramework") + { + executable = Path.Combine( + toolsPath, + "net451", + startupProject.PlatformTarget == "x86" + ? "ef.x86.exe" + : "ef.exe"); + } + else if (targetFramework.Identifier == ".NETCoreApp" + || targetFramework.Identifier == ".NETStandard") + { + if (targetFramework.Identifier == ".NETStandard") + { + Reporter.WriteWarning(Resources.NETStandardStartupProject(startupProject.ProjectName)); + } + + executable = "dotnet"; + args.Add("exec"); + args.Add("--depsfile"); + args.Add(depsFile); + + if (!string.IsNullOrEmpty(projectAssetsFile)) + { + using (var reader = new JsonTextReader(File.OpenText(projectAssetsFile))) + { + var projectAssets = JObject.ReadFrom(reader); + var packageFolders = projectAssets["packageFolders"].Children().Select(p => p.Name); + + foreach (var packageFolder in packageFolders) + { + args.Add("--additionalprobingpath"); + args.Add(packageFolder.TrimEnd(Path.DirectorySeparatorChar)); + } + } + } + + if (File.Exists(runtimeConfig)) + { + args.Add("--runtimeconfig"); + args.Add(runtimeConfig); + } + + args.Add(Path.Combine(toolsPath, "netcoreapp1.0", "ef.dll")); + } + else + { + throw new CommandException( + Resources.UnsupportedFramework(startupProject.ProjectName, targetFramework.Identifier)); + } + + args.AddRange(_args); + args.Add("--assembly"); + args.Add(targetPath); + args.Add("--startup-assembly"); + args.Add(startupTargetPath); + args.Add("--project-dir"); + args.Add(project.ProjectDir); + args.Add("--content-root"); + args.Add(startupProject.ProjectDir); + args.Add("--data-dir"); + args.Add(targetDir); + + if (Reporter.IsVerbose) + { + args.Add("--verbose"); + } + + if (Reporter.NoColor) + { + args.Add("--no-color"); + } + + if (Reporter.PrefixOutput) + { + args.Add("--prefix-output"); + } + + if (project.RootNamespace.Length != 0) + { + args.Add("--root-namespace"); + args.Add(project.RootNamespace); + } + + return Exe.Run(executable, args); + } + + private static string FindProjects( + string path, + string errorWhenNoProject, + string errorWhenMultipleProjects) + { + var specified = true; + if (path == null) + { + specified = false; + path = Directory.GetCurrentDirectory(); + } + else if (!Directory.Exists(path)) // It's not a directory + { + return path; + } + + var projectFiles = Directory.EnumerateFiles(path, "*.*proj", SearchOption.TopDirectoryOnly) + .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase)) + .Take(2).ToList(); + if (projectFiles.Count == 0) + { + throw new CommandException( + specified + ? Resources.NoProjectInDirectory(path) + : errorWhenNoProject); + } + if (projectFiles.Count != 1) + { + throw new CommandException( + specified + ? Resources.MultipleProjectsInDirectory(path) + : errorWhenMultipleProjects); + } + + return projectFiles[0]; + } + + private static string GetVersion() + => typeof(RootCommand).GetTypeInfo().Assembly.GetCustomAttribute() + .InformationalVersion; + + private static bool ShouldHelp(IReadOnlyList commands) + => commands.Count == 0 + || (commands.Count == 1 + && (commands[0] == "database" + || commands[0] == "dbcontext" + || commands[0] == "migrations")); + + private int ShowHelp(bool help, IEnumerable commands) + { + var app = new CommandLineApplication + { + Name = _command.Name + }; + + new EFCommand().Configure(app); + + app.FullName = _command.FullName; + + var args = new List(commands); + if (help) + { + args.Add("--help"); + } + + return app.Execute(args.ToArray()); + } + } +} diff --git a/src/dotnet-ef/dotnet-ef.csproj b/src/dotnet-ef/dotnet-ef.csproj new file mode 100644 index 0000000000..bbe210e0ab --- /dev/null +++ b/src/dotnet-ef/dotnet-ef.csproj @@ -0,0 +1,62 @@ + + + + + + Entity Framework Core .NET Command Line Tools + netcoreapp1.0 + Exe + false + Microsoft.EntityFrameworkCore.Tools + 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextTemplatingFileGenerator + Resources.Designer.cs + + + + + + + + + + True + True + Resources.Designer.tt + + + + + + + + \ No newline at end of file diff --git a/src/ef/AnsiConsole.cs b/src/ef/AnsiConsole.cs new file mode 100644 index 0000000000..24b1ae9b04 --- /dev/null +++ b/src/ef/AnsiConsole.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class AnsiConsole + { + public static readonly AnsiTextWriter _out = new AnsiTextWriter(Console.Out); + + public static void WriteLine(string text) + => _out.WriteLine(text); + } +} diff --git a/src/ef/AnsiConstants.cs b/src/ef/AnsiConstants.cs new file mode 100644 index 0000000000..ffa0b88b12 --- /dev/null +++ b/src/ef/AnsiConstants.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class AnsiConstants + { + public const string Reset = "\x1b[22m\x1b[39m"; + public const string Bold = "\x1b[1m"; + public const string Dark = "\x1b[22m"; + public const string Black = "\x1b[30m"; + public const string Red = "\x1b[31m"; + public const string Green = "\x1b[32m"; + public const string Yellow = "\x1b[33m"; + public const string Blue = "\x1b[34m"; + public const string Magenta = "\x1b[35m"; + public const string Cyan = "\x1b[36m"; + public const string Gray = "\x1b[37m"; + } +} diff --git a/src/ef/AnsiTextWriter.cs b/src/ef/AnsiTextWriter.cs new file mode 100644 index 0000000000..aeb7816d0b --- /dev/null +++ b/src/ef/AnsiTextWriter.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class AnsiTextWriter + { + private readonly TextWriter _writer; + + public AnsiTextWriter(TextWriter writer) + { + _writer = writer; + } + + public void WriteLine(string text) + { + Interpret(text); + _writer.Write(Environment.NewLine); + } + + private void Interpret(string value) + { + var matches = Regex.Matches(value, "\x1b\\[([0-9]+)?m"); + + var start = 0; + foreach (Match match in matches) + { + var length = match.Index - start; + if (length != 0) + { + _writer.Write(value.Substring(start, length)); + } + + Apply(match.Groups[1].Value); + + start = match.Index + match.Length; + } + + if (start != value.Length) + { + _writer.Write(value.Substring(start)); + } + } + + private static void Apply(string parameter) + { + switch (parameter) + { + case "1": + ApplyBold(); + break; + + case "22": + ResetBold(); + break; + + case "30": + ApplyColor(ConsoleColor.Black); + break; + + case "31": + ApplyColor(ConsoleColor.DarkRed); + break; + + case "32": + ApplyColor(ConsoleColor.DarkGreen); + break; + + case "33": + ApplyColor(ConsoleColor.DarkYellow); + break; + + case "34": + ApplyColor(ConsoleColor.DarkBlue); + break; + + case "35": + ApplyColor(ConsoleColor.DarkMagenta); + break; + + case "36": + ApplyColor(ConsoleColor.DarkCyan); + break; + + case "37": + ApplyColor(ConsoleColor.Gray); + break; + + case "39": + ResetColor(); + break; + + default: + Debug.Fail("Unsupported parameter: " + parameter); + break; + } + } + + private static void ApplyBold() + => Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor | 8); + + private static void ResetBold() + => Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor & 7); + + private static void ApplyColor(ConsoleColor color) + { + var wasBold = ((int)Console.ForegroundColor & 8) != 0; + + Console.ForegroundColor = color; + + if (wasBold) + { + ApplyBold(); + } + } + + private static void ResetColor() + { + var wasBold = ((int)Console.ForegroundColor & 8) != 0; + + Console.ResetColor(); + + if (wasBold) + { + ApplyBold(); + } + } + } +} diff --git a/src/ef/AppDomainOperationExecutor.cs b/src/ef/AppDomainOperationExecutor.cs new file mode 100644 index 0000000000..7e82703d77 --- /dev/null +++ b/src/ef/AppDomainOperationExecutor.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET451 + +using System; +using System.Collections; +using System.IO; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class AppDomainOperationExecutor : OperationExecutorBase + { + private readonly object _executor; + private readonly AppDomain _domain; + private bool _disposed; + + public AppDomainOperationExecutor( + string assembly, + string startupAssembly, + string projectDir, + string contentRootPath, + string dataDirectory, + string rootNamespace, + string environment) + : base(assembly, startupAssembly, projectDir, contentRootPath, dataDirectory, rootNamespace, environment) + { + var info = new AppDomainSetup { ApplicationBase = AppBasePath }; + + var configurationFile = (startupAssembly ?? assembly) + ".config"; + if (File.Exists(configurationFile)) + { + Reporter.WriteVerbose(Resources.UsingConfigurationFile(configurationFile)); + info.ConfigurationFile = configurationFile; + } + + _domain = AppDomain.CreateDomain("EntityFrameworkCore.DesignDomain", null, info); + + if (dataDirectory != null) + { + _domain.SetData("DataDirectory", dataDirectory); + } + + var reportHandler = new OperationReportHandler( + Reporter.WriteError, + Reporter.WriteWarning, + Reporter.WriteInformation, + Reporter.WriteVerbose); + + _executor = _domain.CreateInstanceAndUnwrap( + DesignAssemblyName, + ExecutorTypeName, + false, + BindingFlags.Default, + null, + new object[] + { + reportHandler, + new Hashtable + { + { "targetName", AssemblyFileName }, + { "startupTargetName", StartupAssemblyFileName }, + { "projectDir", ProjectDirectory }, + { "contentRootPath", ContentRootPath }, + { "rootNamespace", RootNamespace }, + { "environment", EnvironmentName } + } + }, + null, + null); + } + + protected override object CreateResultHandler() + => new OperationResultHandler(); + + protected override void Execute(string operationName, object resultHandler, IDictionary arguments) + => _domain.CreateInstance( + DesignAssemblyName, + ExecutorTypeName + "+" + operationName, + false, + BindingFlags.Default, + null, + new[] { _executor, resultHandler, arguments }, + null, + null); + + public override void Dispose() + { + if (!_disposed) + { + _disposed = true; + AppDomain.Unload(_domain); + } + } + } +} + +#endif diff --git a/src/ef/CommandException.cs b/src/ef/CommandException.cs new file mode 100644 index 0000000000..74d011b871 --- /dev/null +++ b/src/ef/CommandException.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class CommandException : Exception + { + public CommandException(string message) + : base(message) + { + } + } +} diff --git a/src/ef/CommandLineUtils/CommandArgument.cs b/src/ef/CommandLineUtils/CommandArgument.cs new file mode 100644 index 0000000000..52450b1df2 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandArgument.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandArgument + { + public CommandArgument() + { + Values = new List(); + } + + public string Name { get; set; } + public string Description { get; set; } + public List Values { get; private set; } + public bool MultipleValues { get; set; } + public string Value + { + get + { + return Values.FirstOrDefault(); + } + } + } +} diff --git a/src/ef/CommandLineUtils/CommandLineApplication.cs b/src/ef/CommandLineUtils/CommandLineApplication.cs new file mode 100644 index 0000000000..b4db6116e7 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandLineApplication.cs @@ -0,0 +1,636 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandLineApplication + { + private enum ParseOptionResult + { + Succeeded, + ShowHelp, + ShowVersion, + UnexpectedArgs, + } + + // Indicates whether the parser should throw an exception when it runs into an unexpected argument. + // If this field is set to false, the parser will stop parsing when it sees an unexpected argument, and all + // remaining arguments, including the first unexpected argument, will be stored in RemainingArguments property. + private readonly bool _throwOnUnexpectedArg; + + public CommandLineApplication(bool throwOnUnexpectedArg = true) + { + _throwOnUnexpectedArg = throwOnUnexpectedArg; + Options = new List(); + Arguments = new List(); + Commands = new List(); + RemainingArguments = new List(); + Invoke = () => 0; + } + + public CommandLineApplication Parent { get; set; } + public string Name { get; set; } + public string FullName { get; set; } + public string Syntax { get; set; } + public string Description { get; set; } + public List Options { get; private set; } + public CommandOption OptionHelp { get; private set; } + public CommandOption OptionVersion { get; private set; } + public List Arguments { get; private set; } + public List RemainingArguments { get; private set; } + public bool IsShowingInformation { get; protected set; } // Is showing help or version? + public Func Invoke { get; set; } + public Func LongVersionGetter { get; set; } + public Func ShortVersionGetter { get; set; } + public List Commands { get; private set; } + public bool HandleResponseFiles { get; set; } + public bool AllowArgumentSeparator { get; set; } + public bool HandleRemainingArguments { get; set; } + public string ArgumentSeparatorHelpText { get; set; } + + public CommandLineApplication Command(string name, bool throwOnUnexpectedArg = true) + { + return Command(name, _ => { }, throwOnUnexpectedArg); + } + + public CommandLineApplication Command(string name, Action configuration, + bool throwOnUnexpectedArg = true) + { + var command = new CommandLineApplication(throwOnUnexpectedArg) { Name = name, Parent = this }; + Commands.Add(command); + configuration(command); + return command; + } + + public CommandOption Option(string template, string description, CommandOptionType optionType) + { + return Option(template, description, optionType, _ => { }); + } + + public CommandOption Option(string template, string description, CommandOptionType optionType, Action configuration) + { + var option = new CommandOption(template, optionType) { Description = description }; + Options.Add(option); + configuration(option); + return option; + } + + public CommandArgument Argument(string name, string description, bool multipleValues = false) + { + return Argument(name, description, _ => { }, multipleValues); + } + + public CommandArgument Argument(string name, string description, Action configuration, bool multipleValues = false) + { + var lastArg = Arguments.LastOrDefault(); + if (lastArg != null && lastArg.MultipleValues) + { + var message = string.Format("The last argument '{0}' accepts multiple values. No more argument can be added.", + lastArg.Name); + throw new InvalidOperationException(message); + } + + var argument = new CommandArgument { Name = name, Description = description, MultipleValues = multipleValues }; + Arguments.Add(argument); + configuration(argument); + return argument; + } + + public void OnExecute(Func invoke) + { + Invoke = invoke; + } + + public void OnExecute(Func> invoke) + { + Invoke = () => invoke().Result; + } + + public int Execute(params string[] args) + { + CommandLineApplication command = this; + IEnumerator arguments = null; + + if (HandleResponseFiles) + { + args = ExpandResponseFiles(args).ToArray(); + } + + for (var index = 0; index < args.Length; index++) + { + var arg = args[index]; + + bool isLongOption = arg.StartsWith("--"); + if (isLongOption || arg.StartsWith("-")) + { + CommandOption option; + var result = ParseOption(isLongOption, command, args, ref index, out option); + if (result == ParseOptionResult.ShowHelp) + { + command.ShowHelp(); + return 0; + } + else if (result == ParseOptionResult.ShowVersion) + { + command.ShowVersion(); + return 0; + } + } + else + { + var subcommand = ParseSubCommand(arg, command); + if (subcommand != null) + { + command = subcommand; + } + else + { + if (arguments == null) + { + arguments = new CommandArgumentEnumerator(command.Arguments.GetEnumerator()); + } + + if (arguments.MoveNext()) + { + arguments.Current.Values.Add(arg); + } + else + { + HandleUnexpectedArg(command, args, index, argTypeName: "command or argument"); + } + } + } + } + + return command.Invoke(); + } + + private ParseOptionResult ParseOption( + bool isLongOption, + CommandLineApplication command, + string[] args, + ref int index, + out CommandOption option) + { + option = null; + ParseOptionResult result = ParseOptionResult.Succeeded; + var arg = args[index]; + + int optionPrefixLength = isLongOption ? 2 : 1; + string[] optionComponents = arg.Substring(optionPrefixLength).Split(new[] { ':', '=' }, 2); + string optionName = optionComponents[0]; + + if (isLongOption) + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.LongName, optionName, StringComparison.Ordinal)); + } + else + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.ShortName, optionName, StringComparison.Ordinal)); + + if (option == null) + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.SymbolName, optionName, StringComparison.Ordinal)); + } + } + + if (option == null) + { + if (isLongOption && string.IsNullOrEmpty(optionName) && + !command._throwOnUnexpectedArg && AllowArgumentSeparator) + { + // a stand-alone "--" is the argument separator, so skip it and + // handle the rest of the args as unexpected args + index++; + } + + HandleUnexpectedArg(command, args, index, argTypeName: "option"); + result = ParseOptionResult.UnexpectedArgs; + } + else if (command.OptionHelp == option) + { + result = ParseOptionResult.ShowHelp; + } + else if (command.OptionVersion == option) + { + result = ParseOptionResult.ShowVersion; + } + else + { + if (optionComponents.Length == 2) + { + if (!option.TryParse(optionComponents[1])) + { + command.ShowHint(); + throw new CommandParsingException(command, + $"Unexpected value '{optionComponents[1]}' for option '{optionName}'"); + } + } + else + { + if (option.OptionType == CommandOptionType.NoValue || + option.OptionType == CommandOptionType.BoolValue) + { + // No value is needed for this option + option.TryParse(null); + } + else + { + index++; + arg = args[index]; + if (!option.TryParse(arg)) + { + command.ShowHint(); + throw new CommandParsingException(command, $"Unexpected value '{arg}' for option '{optionName}'"); + + } + } + } + } + + return result; + } + + private CommandLineApplication ParseSubCommand(string arg, CommandLineApplication command) + { + foreach (var subcommand in command.Commands) + { + if (string.Equals(subcommand.Name, arg, StringComparison.OrdinalIgnoreCase)) + { + return subcommand; + } + } + + return null; + } + + // Helper method that adds a help option + public CommandOption HelpOption(string template) + { + // Help option is special because we stop parsing once we see it + // So we store it separately for further use + OptionHelp = Option(template, "Show help information", CommandOptionType.NoValue); + + return OptionHelp; + } + + public CommandOption VersionOption(string template, + string shortFormVersion, + string longFormVersion = null) + { + if (longFormVersion == null) + { + return VersionOption(template, () => shortFormVersion); + } + else + { + return VersionOption(template, () => shortFormVersion, () => longFormVersion); + } + } + + // Helper method that adds a version option + public CommandOption VersionOption(string template, + Func shortFormVersionGetter, + Func longFormVersionGetter = null) + { + // Version option is special because we stop parsing once we see it + // So we store it separately for further use + OptionVersion = Option(template, "Show version information", CommandOptionType.NoValue); + ShortVersionGetter = shortFormVersionGetter; + LongVersionGetter = longFormVersionGetter ?? shortFormVersionGetter; + + return OptionVersion; + } + + // Show short hint that reminds users to use help option + public void ShowHint() + { + if (OptionHelp != null) + { + Console.WriteLine(string.Format("Specify --{0} for a list of available options and commands.", OptionHelp.LongName)); + } + } + + // Show full help + public void ShowHelp(string commandName = null) + { + var headerBuilder = new StringBuilder("Usage:"); + var usagePrefixLength = headerBuilder.Length; + for (var cmd = this; cmd != null; cmd = cmd.Parent) + { + cmd.IsShowingInformation = true; + if (cmd != this && cmd.Arguments.Any()) + { + var args = string.Join(" ", cmd.Arguments.Select(arg => arg.Name)); + headerBuilder.Insert(usagePrefixLength, string.Format(" {0} {1}", cmd.Name, args)); + } + else + { + headerBuilder.Insert(usagePrefixLength, string.Format(" {0}", cmd.Name)); + } + } + + CommandLineApplication target; + + if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase)) + { + target = this; + } + else + { + target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase)); + + if (target != null) + { + headerBuilder.AppendFormat(" {0}", commandName); + } + else + { + // The command name is invalid so don't try to show help for something that doesn't exist + target = this; + } + + } + + var optionsBuilder = new StringBuilder(); + var commandsBuilder = new StringBuilder(); + var argumentsBuilder = new StringBuilder(); + var argumentSeparatorBuilder = new StringBuilder(); + + int maxArgLen = 0; + for (var cmd = target; cmd != null; cmd = cmd.Parent) + { + if (cmd.Arguments.Any()) + { + if (cmd == target) + { + headerBuilder.Append(" [arguments]"); + } + + if (argumentsBuilder.Length == 0) + { + argumentsBuilder.AppendLine(); + argumentsBuilder.AppendLine("Arguments:"); + } + + maxArgLen = Math.Max(maxArgLen, MaxArgumentLength(cmd.Arguments)); + } + } + + for (var cmd = target; cmd != null; cmd = cmd.Parent) + { + if (cmd.Arguments.Any()) + { + var outputFormat = " {0}{1}"; + foreach (var arg in cmd.Arguments) + { + argumentsBuilder.AppendFormat( + outputFormat, + arg.Name.PadRight(maxArgLen + 2), + arg.Description); + argumentsBuilder.AppendLine(); + } + } + } + + if (target.Options.Any()) + { + headerBuilder.Append(" [options]"); + + optionsBuilder.AppendLine(); + optionsBuilder.AppendLine("Options:"); + var maxOptLen = MaxOptionTemplateLength(target.Options); + var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2); + foreach (var opt in target.Options) + { + optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description); + optionsBuilder.AppendLine(); + } + } + + if (target.Commands.Any()) + { + headerBuilder.Append(" [command]"); + + commandsBuilder.AppendLine(); + commandsBuilder.AppendLine("Commands:"); + var maxCmdLen = MaxCommandLength(target.Commands); + var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2); + foreach (var cmd in target.Commands.OrderBy(c => c.Name)) + { + commandsBuilder.AppendFormat(outputFormat, cmd.Name, cmd.Description); + commandsBuilder.AppendLine(); + } + + if (OptionHelp != null) + { + commandsBuilder.AppendLine(); + commandsBuilder.AppendFormat("Use \"{0} [command] --help\" for more information about a command.", Name); + commandsBuilder.AppendLine(); + } + } + + if (target.AllowArgumentSeparator || target.HandleRemainingArguments) + { + if (target.AllowArgumentSeparator) + { + headerBuilder.Append(" [[--] ...]]"); + } + else + { + headerBuilder.Append(" [args]"); + } + + if (!string.IsNullOrEmpty(target.ArgumentSeparatorHelpText)) + { + argumentSeparatorBuilder.AppendLine(); + argumentSeparatorBuilder.AppendLine("Args:"); + argumentSeparatorBuilder.AppendLine($" {target.ArgumentSeparatorHelpText}"); + argumentSeparatorBuilder.AppendLine(); + } + } + + headerBuilder.AppendLine(); + + var nameAndVersion = new StringBuilder(); + nameAndVersion.AppendLine(GetFullNameAndVersion()); + nameAndVersion.AppendLine(); + + Console.Write("{0}{1}{2}{3}{4}{5}", nameAndVersion, headerBuilder, argumentsBuilder, optionsBuilder, commandsBuilder, argumentSeparatorBuilder); + } + + public void ShowVersion() + { + for (var cmd = this; cmd != null; cmd = cmd.Parent) + { + cmd.IsShowingInformation = true; + } + + Console.WriteLine(FullName); + Console.WriteLine(LongVersionGetter()); + } + + public string GetFullNameAndVersion() + { + return ShortVersionGetter == null ? FullName : string.Format("{0} {1}", FullName, ShortVersionGetter()); + } + + public void ShowRootCommandFullNameAndVersion() + { + var rootCmd = this; + while (rootCmd.Parent != null) + { + rootCmd = rootCmd.Parent; + } + + Console.WriteLine(rootCmd.GetFullNameAndVersion()); + Console.WriteLine(); + } + + private int MaxOptionTemplateLength(IEnumerable options) + { + var maxLen = 0; + foreach (var opt in options) + { + maxLen = opt.Template.Length > maxLen ? opt.Template.Length : maxLen; + } + return maxLen; + } + + private int MaxCommandLength(IEnumerable commands) + { + var maxLen = 0; + foreach (var cmd in commands) + { + maxLen = cmd.Name.Length > maxLen ? cmd.Name.Length : maxLen; + } + return maxLen; + } + + private int MaxArgumentLength(IEnumerable arguments) + { + var maxLen = 0; + foreach (var arg in arguments) + { + maxLen = arg.Name.Length > maxLen ? arg.Name.Length : maxLen; + } + return maxLen; + } + + private void HandleUnexpectedArg(CommandLineApplication command, string[] args, int index, string argTypeName) + { + if (command._throwOnUnexpectedArg) + { + command.ShowHint(); + throw new CommandParsingException(command, $"Unrecognized {argTypeName} '{args[index]}'"); + } + else + { + command.RemainingArguments.Add(args[index]); + } + } + + private IEnumerable ExpandResponseFiles(IEnumerable args) + { + foreach (var arg in args) + { + if (!arg.StartsWith("@", StringComparison.Ordinal)) + { + yield return arg; + } + else + { + var fileName = arg.Substring(1); + + var responseFileArguments = ParseResponseFile(fileName); + + // ParseResponseFile can suppress expanding this response file by + // returning null. In that case, we'll treat the response + // file token as a regular argument. + + if (responseFileArguments == null) + { + yield return arg; + } + else + { + foreach (var responseFileArgument in responseFileArguments) + yield return responseFileArgument.Trim(); + } + } + } + } + + private IEnumerable ParseResponseFile(string fileName) + { + if (!HandleResponseFiles) + return null; + + if (!File.Exists(fileName)) + { + throw new InvalidOperationException($"Response file '{fileName}' doesn't exist."); + } + + return File.ReadLines(fileName); + } + + private class CommandArgumentEnumerator : IEnumerator + { + private readonly IEnumerator _enumerator; + + public CommandArgumentEnumerator(IEnumerator enumerator) + { + _enumerator = enumerator; + } + + public CommandArgument Current + { + get + { + return _enumerator.Current; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + _enumerator.Dispose(); + } + + public bool MoveNext() + { + if (Current == null || !Current.MultipleValues) + { + return _enumerator.MoveNext(); + } + + // If current argument allows multiple values, we don't move forward and + // all later values will be added to current CommandArgument.Values + return true; + } + + public void Reset() + { + _enumerator.Reset(); + } + } + } +} diff --git a/src/ef/CommandLineUtils/CommandLineApplicationExtensions.cs b/src/ef/CommandLineUtils/CommandLineApplicationExtensions.cs new file mode 100644 index 0000000000..1c43455ee1 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandLineApplicationExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal static class CommandLineApplicationExtensions + { + public static CommandOption Option(this CommandLineApplication command, string template, string description) + => command.Option( + template, + description, + template.IndexOf('<') != -1 + ? template.EndsWith(">...") + ? CommandOptionType.MultipleValue + : CommandOptionType.SingleValue + : CommandOptionType.NoValue); + } +} diff --git a/src/ef/CommandLineUtils/CommandOption.cs b/src/ef/CommandLineUtils/CommandOption.cs new file mode 100644 index 0000000000..b686f7e2c3 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandOption.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandOption + { + public CommandOption(string template, CommandOptionType optionType) + { + Template = template; + OptionType = optionType; + Values = new List(); + + foreach (var part in Template.Split(new[] { ' ', '|' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (part.StartsWith("--")) + { + LongName = part.Substring(2); + } + else if (part.StartsWith("-")) + { + var optName = part.Substring(1); + + // If there is only one char and it is not an English letter, it is a symbol option (e.g. "-?") + if (optName.Length == 1 && !IsEnglishLetter(optName[0])) + { + SymbolName = optName; + } + else + { + ShortName = optName; + } + } + else if (part.StartsWith("<") && part.EndsWith(">")) + { + ValueName = part.Substring(1, part.Length - 2); + } + else if (optionType == CommandOptionType.MultipleValue && part.StartsWith("<") && part.EndsWith(">...")) + { + ValueName = part.Substring(1, part.Length - 5); + } + else + { + throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template)); + } + } + + if (string.IsNullOrEmpty(LongName) && string.IsNullOrEmpty(ShortName) && string.IsNullOrEmpty(SymbolName)) + { + throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template)); + } + } + + public string Template { get; set; } + public string ShortName { get; set; } + public string LongName { get; set; } + public string SymbolName { get; set; } + public string ValueName { get; set; } + public string Description { get; set; } + public List Values { get; private set; } + public bool? BoolValue { get; private set; } + public CommandOptionType OptionType { get; private set; } + + public bool TryParse(string value) + { + switch (OptionType) + { + case CommandOptionType.MultipleValue: + Values.Add(value); + break; + case CommandOptionType.SingleValue: + if (Values.Any()) + { + return false; + } + Values.Add(value); + break; + case CommandOptionType.BoolValue: + if (Values.Any()) + { + return false; + } + + if (value == null) + { + // add null to indicate that the option was present, but had no value + Values.Add(null); + BoolValue = true; + } + else + { + bool boolValue; + if (!bool.TryParse(value, out boolValue)) + { + return false; + } + + Values.Add(value); + BoolValue = boolValue; + } + break; + case CommandOptionType.NoValue: + if (value != null) + { + return false; + } + // Add a value to indicate that this option was specified + Values.Add("on"); + break; + default: + break; + } + return true; + } + + public bool HasValue() + { + return Values.Any(); + } + + public string Value() + { + return HasValue() ? Values[0] : null; + } + + private bool IsEnglishLetter(char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + } +} \ No newline at end of file diff --git a/src/ef/CommandLineUtils/CommandOptionType.cs b/src/ef/CommandLineUtils/CommandOptionType.cs new file mode 100644 index 0000000000..5f7d37f029 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandOptionType.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal enum CommandOptionType + { + MultipleValue, + SingleValue, + BoolValue, + NoValue + } +} diff --git a/src/ef/CommandLineUtils/CommandParsingException.cs b/src/ef/CommandLineUtils/CommandParsingException.cs new file mode 100644 index 0000000000..09f6d69181 --- /dev/null +++ b/src/ef/CommandLineUtils/CommandParsingException.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandParsingException : Exception + { + public CommandParsingException(CommandLineApplication command, string message) + : base(message) + { + Command = command; + } + + public CommandLineApplication Command { get; } + } +} diff --git a/src/ef/Commands/CommandBase.cs b/src/ef/Commands/CommandBase.cs new file mode 100644 index 0000000000..626b9e30b2 --- /dev/null +++ b/src/ef/Commands/CommandBase.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal abstract class CommandBase + { + public virtual void Configure(CommandLineApplication command) + { + var verbose = command.Option("-v|--verbose", Resources.VerboseDescription); + var noColor = command.Option("--no-color", Resources.NoColorDescription); + var prefixOutput = command.Option("--prefix-output", Resources.PrefixDescription); + + command.OnExecute( + () => + { + Reporter.IsVerbose = verbose.HasValue(); + Reporter.NoColor = noColor.HasValue(); + Reporter.PrefixOutput = prefixOutput.HasValue(); + + Validate(); + + return Execute(); + }); + } + + protected virtual void Validate() + { + } + + protected virtual int Execute() + => 0; + } +} diff --git a/src/ef/Commands/ContextCommandBase.cs b/src/ef/Commands/ContextCommandBase.cs new file mode 100644 index 0000000000..168f5daf9e --- /dev/null +++ b/src/ef/Commands/ContextCommandBase.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class ContextCommandBase : ProjectCommandBase + { + private CommandOption _context; + + protected CommandOption Context + => _context; + + public override void Configure(CommandLineApplication command) + { + _context = command.Option("-c|--context ", Resources.ContextDescription); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DatabaseCommand.cs b/src/ef/Commands/DatabaseCommand.cs new file mode 100644 index 0000000000..ccff76f6cd --- /dev/null +++ b/src/ef/Commands/DatabaseCommand.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class DatabaseCommand : HelpCommandBase + { + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DatabaseDescription; + + command.Command("drop", new DatabaseDropCommand().Configure); + command.Command("update", new DatabaseUpdateCommand().Configure); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DatabaseDropCommand.Configure.cs b/src/ef/Commands/DatabaseDropCommand.Configure.cs new file mode 100644 index 0000000000..8c899ee65d --- /dev/null +++ b/src/ef/Commands/DatabaseDropCommand.Configure.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class DatabaseDropCommand : ContextCommandBase + { + private CommandOption _force; + private CommandOption _dryRun; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DatabaseDropDescription; + + _force = command.Option("-f|--force", Resources.DatabaseDropForceDescription); + _dryRun = command.Option("--dry-run", Resources.DatabaseDropDryRunDescription); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DatabaseDropCommand.cs b/src/ef/Commands/DatabaseDropCommand.cs new file mode 100644 index 0000000000..bbf3418225 --- /dev/null +++ b/src/ef/Commands/DatabaseDropCommand.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class DatabaseDropCommand + { + protected override int Execute() + { + var executor = CreateExecutor(); + + var result = executor.GetContextInfo(Context.Value()); + var databaseName = result["DatabaseName"] as string; + var dataSource = result["DataSource"] as string; + + if (_dryRun.HasValue()) + { + Reporter.WriteInformation(Resources.DatabaseDropDryRun(databaseName, dataSource)); + + return 0; + } + + if (!_force.HasValue()) + { + Reporter.WriteInformation(Resources.DatabaseDropPrompt(databaseName, dataSource)); + var response = Console.ReadLine().Trim().ToUpperInvariant(); + if (response != "Y") + { + return 1; + } + } + + executor.DropDatabase(Context.Value()); + + return base.Execute(); + } + } +} diff --git a/src/ef/Commands/DatabaseUpdateCommand.Configure.cs b/src/ef/Commands/DatabaseUpdateCommand.Configure.cs new file mode 100644 index 0000000000..633fa58da2 --- /dev/null +++ b/src/ef/Commands/DatabaseUpdateCommand.Configure.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class DatabaseUpdateCommand : ContextCommandBase + { + private CommandArgument _migration; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DatabaseUpdateDescription; + + _migration = command.Argument("", Resources.MigrationDescription); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DatabaseUpdateCommand.cs b/src/ef/Commands/DatabaseUpdateCommand.cs new file mode 100644 index 0000000000..c23f1e7104 --- /dev/null +++ b/src/ef/Commands/DatabaseUpdateCommand.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class DatabaseUpdateCommand + { + protected override int Execute() + { + CreateExecutor().UpdateDatabase(_migration.Value, Context.Value()); + + return base.Execute(); + } + } +} diff --git a/src/ef/Commands/DbContextCommand.cs b/src/ef/Commands/DbContextCommand.cs new file mode 100644 index 0000000000..dc47be6b02 --- /dev/null +++ b/src/ef/Commands/DbContextCommand.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class DbContextCommand : HelpCommandBase + { + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DbContextDescription; + + command.Command("info", new DbContextInfoCommand().Configure); + command.Command("list", new DbContextListCommand().Configure); + command.Command("scaffold", new DbContextScaffoldCommand().Configure); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DbContextInfoCommand.Configure.cs b/src/ef/Commands/DbContextInfoCommand.Configure.cs new file mode 100644 index 0000000000..641ff97725 --- /dev/null +++ b/src/ef/Commands/DbContextInfoCommand.Configure.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class DbContextInfoCommand : ContextCommandBase + { + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DbContextInfoDescription; + + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DbContextInfoCommand.cs b/src/ef/Commands/DbContextInfoCommand.cs new file mode 100644 index 0000000000..f68c9583cc --- /dev/null +++ b/src/ef/Commands/DbContextInfoCommand.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class DbContextInfoCommand + { + protected override int Execute() + { + var result = CreateExecutor().GetContextInfo(Context.Value()); + + if (_json.HasValue()) + { + ReportJsonResult(result); + } + else + { + ReportResult(result); + } + + return base.Execute(); + } + + private static void ReportJsonResult(IDictionary result) + { + Reporter.WriteData("{"); + Reporter.WriteData(" \"databaseName\": \"" + Json.Escape(result["DatabaseName"] as string) + "\","); + Reporter.WriteData(" \"dataSource\": \"" + Json.Escape(result["DataSource"] as string) + "\""); + Reporter.WriteData("}"); + } + + private static void ReportResult(IDictionary result) + { + Reporter.WriteData(Resources.DatabaseName(result["DatabaseName"])); + Reporter.WriteData(Resources.DataSource(result["DataSource"])); + } + } +} diff --git a/src/ef/Commands/DbContextListCommand.Configure.cs b/src/ef/Commands/DbContextListCommand.Configure.cs new file mode 100644 index 0000000000..47eda37563 --- /dev/null +++ b/src/ef/Commands/DbContextListCommand.Configure.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class DbContextListCommand : ProjectCommandBase + { + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DbContextListDescription; + + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DbContextListCommand.cs b/src/ef/Commands/DbContextListCommand.cs new file mode 100644 index 0000000000..43351441c7 --- /dev/null +++ b/src/ef/Commands/DbContextListCommand.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class DbContextListCommand + { + protected override int Execute() + { + var types = CreateExecutor().GetContextTypes().ToList(); + + if (_json.HasValue()) + { + ReportJsonResults(types); + } + else + { + ReportResults(types); + } + + return base.Execute(); + } + + private static void ReportJsonResults(IReadOnlyList contextTypes) + { + var nameGroups = contextTypes.GroupBy(t => t["Name"]).ToList(); + var fullNameGroups = contextTypes.GroupBy(t => t["FullName"]).ToList(); + + Reporter.WriteData("["); + + for (var i = 0; i < contextTypes.Count; i++) + { + var safeName = nameGroups.Count(g => g.Key == contextTypes[i]["Name"]) == 1 + ? contextTypes[i]["Name"] + : fullNameGroups.Count(g => g.Key == contextTypes[i]["FullName"]) == 1 + ? contextTypes[i]["FullName"] + : contextTypes[i]["AssemblyQualifiedName"]; + + Reporter.WriteData(" {"); + Reporter.WriteData(" \"fullName\": \"" + contextTypes[i]["FullName"] + "\","); + Reporter.WriteData(" \"safeName\": \"" + safeName + "\","); + Reporter.WriteData(" \"name\": \"" + contextTypes[i]["Name"] + "\","); + Reporter.WriteData(" \"assemblyQualifiedName\": \"" + contextTypes[i]["AssemblyQualifiedName"] + "\""); + + var line = " }"; + if (i != contextTypes.Count - 1) + { + line += ","; + } + + Reporter.WriteData(line); + } + + Reporter.WriteData("]"); + } + + private static void ReportResults(IEnumerable contextTypes) + { + var any = false; + foreach (var contextType in contextTypes) + { + Reporter.WriteData(contextType["FullName"] as string); + any = true; + } + + if (!any) + { + Reporter.WriteInformation(Resources.NoDbContext); + } + } + } +} diff --git a/src/ef/Commands/DbContextScaffoldCommand.Configure.cs b/src/ef/Commands/DbContextScaffoldCommand.Configure.cs new file mode 100644 index 0000000000..19314721b3 --- /dev/null +++ b/src/ef/Commands/DbContextScaffoldCommand.Configure.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class DbContextScaffoldCommand : ProjectCommandBase + { + private CommandArgument _connection; + private CommandArgument _provider; + private CommandOption _dataAnnotations; + private CommandOption _context; + private CommandOption _force; + private CommandOption _outputDir; + private CommandOption _schemas; + private CommandOption _tables; + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.DbContextScaffoldDescription; + + _connection = command.Argument("", Resources.ConnectionDescription); + _provider = command.Argument("", Resources.ProviderDescription); + + _dataAnnotations = command.Option("-d|--data-annotations", Resources.DataAnnotationsDescription); + _context = command.Option("-c|--context ", Resources.ContextNameDescription); + _force = command.Option("-f|--force", Resources.DbContextScaffoldForceDescription); + _outputDir = command.Option("-o|--output-dir ", Resources.OutputDirDescription); + _schemas = command.Option("--schema ...", Resources.SchemasDescription); + _tables = command.Option("-t|--table ...", Resources.TablesDescription); + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/DbContextScaffoldCommand.cs b/src/ef/Commands/DbContextScaffoldCommand.cs new file mode 100644 index 0000000000..ce2d0b13cf --- /dev/null +++ b/src/ef/Commands/DbContextScaffoldCommand.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class DbContextScaffoldCommand + { + protected override void Validate() + { + base.Validate(); + + if (string.IsNullOrEmpty(_connection.Value)) + { + throw new CommandException(Resources.MissingArgument(_connection.Name)); + } + if (string.IsNullOrEmpty(_provider.Value)) + { + throw new CommandException(Resources.MissingArgument(_provider.Name)); + } + } + + protected override int Execute() + { + var filesCreated = CreateExecutor().ScaffoldContext( + _provider.Value, + _connection.Value, + _outputDir.Value(), + _context.Value(), + _schemas.Values, + _tables.Values, + _dataAnnotations.HasValue(), + _force.HasValue()) + .ToList(); + if (_json.HasValue()) + { + ReportJsonResults(filesCreated); + } + + return base.Execute(); + } + + private void ReportJsonResults(IReadOnlyList files) + { + Reporter.WriteData("["); + + for (var i = 0; i < files.Count; i++) + { + var line = " \"" + Json.Escape(files[i]) + "\""; + if (i != files.Count - 1) + { + line += ","; + } + + Reporter.WriteData(line); + } + + Reporter.WriteData("]"); + } + } +} diff --git a/src/ef/Commands/EFCommandBase.cs b/src/ef/Commands/EFCommandBase.cs new file mode 100644 index 0000000000..2f671024f1 --- /dev/null +++ b/src/ef/Commands/EFCommandBase.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal abstract class EFCommandBase : CommandBase + { + public override void Configure(CommandLineApplication command) + { + command.HelpOption("-h|--help"); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/EnvironmentCommandBase.cs b/src/ef/Commands/EnvironmentCommandBase.cs new file mode 100644 index 0000000000..4032566506 --- /dev/null +++ b/src/ef/Commands/EnvironmentCommandBase.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class EnvironmentCommandBase : EFCommandBase + { + private CommandOption _environment; + + protected CommandOption Environment + => _environment; + + public override void Configure(CommandLineApplication command) + { + _environment = command.Option("-e|--environment ", Resources.EnvironmentDescription); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/HelpCommandBase.cs b/src/ef/Commands/HelpCommandBase.cs new file mode 100644 index 0000000000..1f44ec2df1 --- /dev/null +++ b/src/ef/Commands/HelpCommandBase.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class HelpCommandBase : EFCommandBase + { + private CommandLineApplication _command; + + public override void Configure(CommandLineApplication command) + { + _command = command; + + base.Configure(command); + } + + protected override int Execute() + { + _command.ShowHelp(); + + return base.Execute(); + } + } +} diff --git a/src/ef/Commands/MigrationsAddCommand.Configure.cs b/src/ef/Commands/MigrationsAddCommand.Configure.cs new file mode 100644 index 0000000000..f80bfbca26 --- /dev/null +++ b/src/ef/Commands/MigrationsAddCommand.Configure.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class MigrationsAddCommand : ContextCommandBase + { + private CommandArgument _name; + private CommandOption _outputDir; + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsAddDescription; + + _name = command.Argument("", Resources.MigrationNameDescription); + + _outputDir = command.Option("-o|--output-dir ", Resources.MigrationsOutputDirDescription); + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/MigrationsAddCommand.cs b/src/ef/Commands/MigrationsAddCommand.cs new file mode 100644 index 0000000000..60f3de60a4 --- /dev/null +++ b/src/ef/Commands/MigrationsAddCommand.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class MigrationsAddCommand + { + protected override void Validate() + { + base.Validate(); + + if (string.IsNullOrEmpty(_name.Value)) + { + throw new CommandException(Resources.MissingArgument(_name.Name)); + } + } + + protected override int Execute() + { + var files = CreateExecutor().AddMigration(_name.Value, _outputDir.Value(), Context.Value()); + + if (_json.HasValue()) + { + ReportJson(files); + } + else + { + Reporter.WriteInformation(Resources.MigrationsAddCompleted); + } + + return base.Execute(); + } + + private static void ReportJson(IDictionary files) + { + Reporter.WriteData("{"); + Reporter.WriteData(" \"migrationFile\": \"" + Json.Escape(files["MigrationFile"] as string) + "\","); + Reporter.WriteData(" \"metadataFile\": \"" + Json.Escape(files["MetadataFile"] as string) + "\","); + Reporter.WriteData(" \"snapshotFile\": \"" + Json.Escape(files["SnapshotFile"] as string) + "\""); + Reporter.WriteData("}"); + } + } +} diff --git a/src/ef/Commands/MigrationsCommand.cs b/src/ef/Commands/MigrationsCommand.cs new file mode 100644 index 0000000000..f8e5ae8145 --- /dev/null +++ b/src/ef/Commands/MigrationsCommand.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class MigrationsCommand : HelpCommandBase + { + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsDescription; + + command.Command("add", new MigrationsAddCommand().Configure); + command.Command("list", new MigrationsListCommand().Configure); + command.Command("remove", new MigrationsRemoveCommand().Configure); + command.Command("script", new MigrationsScriptCommand().Configure); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/MigrationsListCommand.Configure.cs b/src/ef/Commands/MigrationsListCommand.Configure.cs new file mode 100644 index 0000000000..888f463cf1 --- /dev/null +++ b/src/ef/Commands/MigrationsListCommand.Configure.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class MigrationsListCommand : ContextCommandBase + { + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsListDescription; + + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/MigrationsListCommand.cs b/src/ef/Commands/MigrationsListCommand.cs new file mode 100644 index 0000000000..4ccad09e6e --- /dev/null +++ b/src/ef/Commands/MigrationsListCommand.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class MigrationsListCommand + { + protected override int Execute() + { + var migrations = CreateExecutor().GetMigrations(Context.Value()).ToList(); + + if (_json.HasValue()) + { + ReportJsonResults(migrations); + } + else + { + ReportResults(migrations); + } + + return base.Execute(); + } + + private static void ReportJsonResults(IReadOnlyList migrations) + { + var nameGroups = migrations.GroupBy(m => m["Name"]).ToList(); + + Reporter.WriteData("["); + + for (var i = 0; i < migrations.Count; i++) + { + var safeName = nameGroups.Count(g => g.Key == migrations[i]["Name"]) == 1 + ? migrations[i]["Name"] + : migrations[i]["Id"]; + + Reporter.WriteData(" {"); + Reporter.WriteData(" \"id\": \"" + migrations[i]["Id"] + "\","); + Reporter.WriteData(" \"name\": \"" + migrations[i]["Name"] + "\","); + Reporter.WriteData(" \"safeName\": \"" + safeName + "\""); + + var line = " }"; + if (i != migrations.Count - 1) + { + line += ","; + } + + Reporter.WriteData(line); + } + + Reporter.WriteData("]"); + } + + private static void ReportResults(IEnumerable migrations) + { + var any = false; + foreach (var migration in migrations) + { + Reporter.WriteData(migration["Id"] as string); + any = true; + } + + if (!any) + { + Reporter.WriteInformation(Resources.NoMigrations); + } + } + } +} diff --git a/src/ef/Commands/MigrationsRemoveCommand.Configure.cs b/src/ef/Commands/MigrationsRemoveCommand.Configure.cs new file mode 100644 index 0000000000..7c1ab8024e --- /dev/null +++ b/src/ef/Commands/MigrationsRemoveCommand.Configure.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class MigrationsRemoveCommand : ContextCommandBase + { + private CommandOption _force; + private CommandOption _json; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsRemoveDescription; + + _force = command.Option("-f|--force", Resources.MigrationsRemoveForceDescription); + _json = Json.ConfigureOption(command); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/MigrationsRemoveCommand.cs b/src/ef/Commands/MigrationsRemoveCommand.cs new file mode 100644 index 0000000000..5d0b907f19 --- /dev/null +++ b/src/ef/Commands/MigrationsRemoveCommand.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class MigrationsRemoveCommand + { + protected override int Execute() + { + var deletedFiles = CreateExecutor().RemoveMigration(Context.Value(), _force.HasValue()).ToList(); + if (_json.HasValue()) + { + ReportJsonResults(deletedFiles); + } + + return base.Execute(); + } + + private void ReportJsonResults(IReadOnlyList files) + { + Reporter.WriteData("["); + + for (var i = 0; i < files.Count; i++) + { + var line = " \"" + Json.Escape(files[i]) + "\""; + if (i != files.Count - 1) + { + line += ","; + } + + Reporter.WriteData(line); + } + + Reporter.WriteData("]"); + } + } +} diff --git a/src/ef/Commands/MigrationsScriptCommand.Configure.cs b/src/ef/Commands/MigrationsScriptCommand.Configure.cs new file mode 100644 index 0000000000..9ecddc4b3f --- /dev/null +++ b/src/ef/Commands/MigrationsScriptCommand.Configure.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal partial class MigrationsScriptCommand : ContextCommandBase + { + private CommandArgument _from; + private CommandArgument _to; + private CommandOption _output; + private CommandOption _idempotent; + + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsScriptDescription; + + _from = command.Argument("", Resources.MigrationFromDescription); + _to = command.Argument("", Resources.MigrationToDescription); + + _output = command.Option("-o|--output ", Resources.OutputDescription); + _idempotent = command.Option("-i|--idempotent", Resources.IdempotentDescription); + + base.Configure(command); + } + } +} diff --git a/src/ef/Commands/MigrationsScriptCommand.cs b/src/ef/Commands/MigrationsScriptCommand.cs new file mode 100644 index 0000000000..c38e694b80 --- /dev/null +++ b/src/ef/Commands/MigrationsScriptCommand.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + partial class MigrationsScriptCommand + { + protected override int Execute() + { + var sql = CreateExecutor().ScriptMigration( + _from.Value, + _to.Value, + _idempotent.HasValue(), + Context.Value()); + + if (!_output.HasValue()) + { + Reporter.WriteData(sql); + } + else + { + var directory = Path.GetDirectoryName(_output.Value()); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + Reporter.WriteVerbose(Resources.WritingFile(_output.Value())); + File.WriteAllText(_output.Value(), sql, Encoding.UTF8); + } + + return base.Execute(); + } + } +} diff --git a/src/ef/Commands/ProjectCommandBase.cs b/src/ef/Commands/ProjectCommandBase.cs new file mode 100644 index 0000000000..040d61b675 --- /dev/null +++ b/src/ef/Commands/ProjectCommandBase.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal abstract class ProjectCommandBase : EnvironmentCommandBase + { + private CommandOption _assembly; + private CommandOption _startupAssembly; + private CommandOption _dataDir; + private CommandOption _projectDir; + private CommandOption _contentRoot; + private CommandOption _rootNamespace; + private CommandOption _noAppDomain; + + public override void Configure(CommandLineApplication command) + { + _assembly = command.Option("-a|--assembly ", Resources.AssemblyDescription); + _noAppDomain = command.Option("--no-appdomain", Resources.NoAppDomainDescription); + _startupAssembly = command.Option("-s|--startup-assembly ", Resources.StartupAssemblyDescription); + _dataDir = command.Option("--data-dir ", Resources.DataDirDescription); + _projectDir = command.Option("--project-dir ", Resources.ProjectDirDescription); + _contentRoot = command.Option("--content-root ", Resources.ContentRootDescription); + _rootNamespace = command.Option("--root-namespace ", Resources.RootNamespaceDescription); + + base.Configure(command); + } + + protected override void Validate() + { + base.Validate(); + + if (!_assembly.HasValue()) + { + throw new CommandException(Resources.MissingOption(_assembly.LongName)); + } + } + + protected IOperationExecutor CreateExecutor() + { + // TODO: Re-throw TypeLoadException and FileNotFoundException? +#if NET451 + if (!_noAppDomain.HasValue()) + { + return new AppDomainOperationExecutor( + _assembly.Value(), + _startupAssembly.Value(), + _projectDir.Value(), + _contentRoot.Value(), + _dataDir.Value(), + _rootNamespace.Value(), + Environment.Value()); + } +#endif + return new ReflectionOperationExecutor( + _assembly.Value(), + _startupAssembly.Value(), + _projectDir.Value(), + _contentRoot.Value(), + _dataDir.Value(), + _rootNamespace.Value(), + Environment.Value()); + } + } +} diff --git a/src/ef/Commands/RootCommand.cs b/src/ef/Commands/RootCommand.cs new file mode 100644 index 0000000000..6f8f8dc9fd --- /dev/null +++ b/src/ef/Commands/RootCommand.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.DotNet.Cli.CommandLine; +using static Microsoft.EntityFrameworkCore.Tools.AnsiConstants; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands +{ + internal class RootCommand : HelpCommandBase + { + public override void Configure(CommandLineApplication command) + { + command.FullName = Resources.EFFullName; + + // NB: Update ShouldHelp in dotnet-ef when adding new command groups + command.Command("database", new DatabaseCommand().Configure); + command.Command("dbcontext", new DbContextCommand().Configure); + command.Command("migrations", new MigrationsCommand().Configure); + + command.VersionOption("--version", GetVersion); + + base.Configure(command); + } + + protected override int Execute() + { + Reporter.WriteInformation( + string.Join( + Environment.NewLine, + string.Empty, + Reporter.Colorize(@" _/\__ ", s => s.Insert(21, Bold + Gray)), + Reporter.Colorize(@" ---==/ \\ ", s => s.Insert(20, Bold + Gray)), + Reporter.Colorize(@" ___ ___ |. \|\ ", s => s.Insert(26, Bold).Insert(21, Dark).Insert(20, Bold + Gray).Insert(9, Dark + Magenta)), + Reporter.Colorize(@" | __|| __| | ) \\\ ", s => s.Insert(20, Bold + Gray).Insert(8, Dark + Magenta)), + Reporter.Colorize(@" | _| | _| \_/ | //|\\ ", s => s.Insert(20, Bold + Gray).Insert(8, Dark + Magenta)), + Reporter.Colorize(@" |___||_| / \\\/\\", s => s.Insert(33, Reset).Insert(23, Bold + Gray).Insert(8, Dark + Magenta)), + string.Empty)); + + return base.Execute(); + } + + private static string GetVersion() + => typeof(RootCommand).GetTypeInfo().Assembly.GetCustomAttribute() + .InformationalVersion; + } +} diff --git a/src/ef/IOperationExecutor.cs b/src/ef/IOperationExecutor.cs new file mode 100644 index 0000000000..4a2339eba4 --- /dev/null +++ b/src/ef/IOperationExecutor.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal interface IOperationExecutor : IDisposable + { + IDictionary AddMigration(string name, string outputDir, string contextType); + IEnumerable RemoveMigration(string contextType, bool force); + IEnumerable GetMigrations(string contextType); + void DropDatabase(string contextType); + IDictionary GetContextInfo(string name); + string GetContextType(string name); + void UpdateDatabase(string migration, string contextType); + IEnumerable GetContextTypes(); + + IEnumerable ScaffoldContext( + string provider, + string connectionString, + string outputDir, + string dbContextClassName, + IEnumerable schemaFilters, + IEnumerable tableFilters, + bool useDataAnnotations, + bool overwriteFiles); + + string ScriptMigration(string fromMigration, string toMigration, bool idempotent, string contextType); + } +} diff --git a/src/ef/Json.cs b/src/ef/Json.cs new file mode 100644 index 0000000000..ba94ce0eb6 --- /dev/null +++ b/src/ef/Json.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class Json + { + public static CommandOption ConfigureOption(CommandLineApplication command) + => command.Option("--json", Resources.JsonDescription); + + public static string Escape(string text) + => text.Replace("\\", "\\\\").Replace("\"", "\\\""); + } +} diff --git a/src/ef/OperationExecutorBase.cs b/src/ef/OperationExecutorBase.cs new file mode 100644 index 0000000000..931116c2f4 --- /dev/null +++ b/src/ef/OperationExecutorBase.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal abstract class OperationExecutorBase : IOperationExecutor + { + private const string DataDirEnvName = "ADONET_DATA_DIR"; + public const string DesignAssemblyName = "Microsoft.EntityFrameworkCore.Design"; + protected const string ExecutorTypeName = "Microsoft.EntityFrameworkCore.Design.OperationExecutor"; + + private static readonly IDictionary EmptyArguments = new Dictionary(0); + public string AppBasePath { get; } + + protected string AssemblyFileName { get; set; } + protected string StartupAssemblyFileName { get; set; } + protected string ContentRootPath { get; } + protected string ProjectDirectory { get; } + protected string RootNamespace { get; } + protected string EnvironmentName { get; } + + protected OperationExecutorBase( + string assembly, + string startupAssembly, + string projectDir, + string contentRootPath, + string dataDirectory, + string rootNamespace, + string environment) + { + AssemblyFileName = Path.GetFileNameWithoutExtension(assembly); + StartupAssemblyFileName = startupAssembly == null + ? AssemblyFileName + : Path.GetFileNameWithoutExtension(startupAssembly); + + AppBasePath = Path.GetDirectoryName(startupAssembly ?? assembly); + if (!Path.IsPathRooted(AppBasePath)) + { + AppBasePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), AppBasePath)); + } + + ContentRootPath = contentRootPath ?? AppBasePath; + RootNamespace = rootNamespace ?? AssemblyFileName; + ProjectDirectory = projectDir ?? Directory.GetCurrentDirectory(); + EnvironmentName = environment; + + Reporter.WriteVerbose(Resources.UsingAssembly(AssemblyFileName)); + Reporter.WriteVerbose(Resources.UsingStartupAssembly(StartupAssemblyFileName)); + Reporter.WriteVerbose(Resources.UsingApplicationBase(AppBasePath)); + Reporter.WriteVerbose(Resources.UsingContentRoot(ContentRootPath)); + Reporter.WriteVerbose(Resources.UsingRootNamespace(RootNamespace)); + Reporter.WriteVerbose(Resources.UsingProjectDir(ProjectDirectory)); + + if (dataDirectory != null) + { + Reporter.WriteVerbose(Resources.UsingDataDir(dataDirectory)); + Environment.SetEnvironmentVariable(DataDirEnvName, dataDirectory); + } + } + + public virtual void Dispose() + { + } + + protected abstract dynamic CreateResultHandler(); + protected abstract void Execute(string operationName, object resultHandler, IDictionary arguments); + + private TResult InvokeOperation(string operation) + => InvokeOperation(operation, EmptyArguments); + + private TResult InvokeOperation(string operation, IDictionary arguments) + => (TResult)InvokeOperationImpl(operation, arguments); + + private void InvokeOperation(string operation, IDictionary arguments) + => InvokeOperationImpl(operation, arguments); + + private object InvokeOperationImpl(string operationName, IDictionary arguments) + { + var resultHandler = CreateResultHandler(); + + var currentDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(AppBasePath); + try + { + Execute(operationName, resultHandler, arguments); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + + if (resultHandler.ErrorType != null) + { + throw new WrappedException( + resultHandler.ErrorType, + resultHandler.ErrorMessage, + resultHandler.ErrorStackTrace); + } + + return resultHandler.Result; + } + + public IDictionary AddMigration(string name, string outputDir, string contextType) + => InvokeOperation("AddMigration", + new Dictionary + { + ["name"] = name, + ["outputDir"] = outputDir, + ["contextType"] = contextType + }); + + public IEnumerable RemoveMigration(string contextType, bool force) + => InvokeOperation>("RemoveMigration", + new Dictionary + { + ["contextType"] = contextType, + ["force"] = force + }); + + public IEnumerable GetMigrations(string contextType) + => InvokeOperation>("GetMigrations", + new Dictionary + { + ["contextType"] = contextType + }); + + public void DropDatabase(string contextType) + => InvokeOperation("DropDatabase", + new Dictionary + { + ["contextType"] = contextType + }); + + public IDictionary GetContextInfo(string name) + => InvokeOperation("GetContextInfo", + new Dictionary + { + ["contextType"] = name + }); + + public void UpdateDatabase(string migration, string contextType) + => InvokeOperation("UpdateDatabase", + new Dictionary + { + ["targetMigration"] = migration, + ["contextType"] = contextType + }); + + public IEnumerable GetContextTypes() + => InvokeOperation>("GetContextTypes"); + + public IEnumerable ScaffoldContext(string provider, + string connectionString, + string outputDir, + string dbContextClassName, + IEnumerable schemaFilters, + IEnumerable tableFilters, + bool useDataAnnotations, + bool overwriteFiles) + => InvokeOperation>("ScaffoldContext", + new Dictionary + { + ["provider"] = provider, + ["connectionString"] = connectionString, + ["outputDir"] = outputDir, + ["dbContextClassName"] = dbContextClassName, + ["schemaFilters"] = schemaFilters, + ["tableFilters"] = tableFilters, + ["useDataAnnotations"] = useDataAnnotations, + ["overwriteFiles"] = overwriteFiles + }); + + public string ScriptMigration( + string fromMigration, + string toMigration, + bool idempotent, + string contextType) + => InvokeOperation("ScriptMigration", + new Dictionary + { + ["fromMigration"] = fromMigration, + ["toMigration"] = toMigration, + ["idempotent"] = idempotent, + ["contextType"] = contextType + }); + + public string GetContextType(string name) + => InvokeOperation("GetContextType", + new Dictionary + { + ["name"] = name + }); + } +} diff --git a/src/ef/Program.cs b/src/ef/Program.cs new file mode 100644 index 0000000000..03eeb328be --- /dev/null +++ b/src/ef/Program.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Commands; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class Program + { + private static int Main(string[] args) + { + var app = new CommandLineApplication() + { + Name = "ef" + }; + + new RootCommand().Configure(app); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { + var wrappedException = ex as WrappedException; + if (ex is CommandException + || ex is CommandParsingException + || (wrappedException != null + && wrappedException.Type == "Microsoft.EntityFrameworkCore.Design.OperationException")) + { + Reporter.WriteVerbose(ex.ToString()); + } + else + { + Reporter.WriteInformation(ex.ToString()); + } + + Reporter.WriteError(ex.Message); + + return 1; + } + } + } +} diff --git a/src/ef/Properties/AssemblyInfo.cs b/src/ef/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f7cf8bb06a --- /dev/null +++ b/src/ef/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo( + "ef.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/ef/Properties/Resources.Designer.cs b/src/ef/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..023051d147 --- /dev/null +++ b/src/ef/Properties/Resources.Designer.cs @@ -0,0 +1,431 @@ +// + +using System.Reflection; +using System.Resources; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Tools.Properties +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.EntityFrameworkCore.Tools.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The assembly to use. + /// + public static string AssemblyDescription + => GetString("AssemblyDescription"); + + /// + /// The connection string to the database. + /// + public static string ConnectionDescription + => GetString("ConnectionDescription"); + + /// + /// The content root path. Defaults to the startup assembly directory. + /// + public static string ContentRootDescription + => GetString("ContentRootDescription"); + + /// + /// The DbContext to use. + /// + public static string ContextDescription + => GetString("ContextDescription"); + + /// + /// The name of the DbContext. + /// + public static string ContextNameDescription + => GetString("ContextNameDescription"); + + /// + /// Use attributes to configure the model (where possible). If omitted, only the fluent API is used. + /// + public static string DataAnnotationsDescription + => GetString("DataAnnotationsDescription"); + + /// + /// Commands to manage the database. + /// + public static string DatabaseDescription + => GetString("DatabaseDescription"); + + /// + /// Drops the database. + /// + public static string DatabaseDropDescription + => GetString("DatabaseDropDescription"); + + /// + /// This would drop the database '{database}' on server '{dataSource}'. + /// + public static string DatabaseDropDryRun([CanBeNull] object database, [CanBeNull] object dataSource) + => string.Format( + GetString("DatabaseDropDryRun", nameof(database), nameof(dataSource)), + database, dataSource); + + /// + /// Show which database would be dropped, but don't drop it. + /// + public static string DatabaseDropDryRunDescription + => GetString("DatabaseDropDryRunDescription"); + + /// + /// Don't confirm. + /// + public static string DatabaseDropForceDescription + => GetString("DatabaseDropForceDescription"); + + /// + /// Are you sure you want to drop the database '{database}' on server '{dataSource}'? (y/N) + /// + public static string DatabaseDropPrompt([CanBeNull] object database, [CanBeNull] object dataSource) + => string.Format( + GetString("DatabaseDropPrompt", nameof(database), nameof(dataSource)), + database, dataSource); + + /// + /// Database name: {database} + /// + public static string DatabaseName([CanBeNull] object database) + => string.Format( + GetString("DatabaseName", nameof(database)), + database); + + /// + /// Updates the database to a specified migration. + /// + public static string DatabaseUpdateDescription + => GetString("DatabaseUpdateDescription"); + + /// + /// The data directory. + /// + public static string DataDirDescription + => GetString("DataDirDescription"); + + /// + /// Data source: {dataSource} + /// + public static string DataSource([CanBeNull] object dataSource) + => string.Format( + GetString("DataSource", nameof(dataSource)), + dataSource); + + /// + /// Commands to manage DbContext types. + /// + public static string DbContextDescription + => GetString("DbContextDescription"); + + /// + /// Gets information about a DbContext type. + /// + public static string DbContextInfoDescription + => GetString("DbContextInfoDescription"); + + /// + /// Lists available DbContext types. + /// + public static string DbContextListDescription + => GetString("DbContextListDescription"); + + /// + /// Scaffolds a DbContext and entity types for a database. + /// + public static string DbContextScaffoldDescription + => GetString("DbContextScaffoldDescription"); + + /// + /// Overwrite existing files. + /// + public static string DbContextScaffoldForceDescription + => GetString("DbContextScaffoldForceDescription"); + + /// + /// Entity Framework Core Command Line Tools + /// + public static string EFFullName + => GetString("EFFullName"); + + /// + /// The environment to use. Defaults to "Development". + /// + public static string EnvironmentDescription + => GetString("EnvironmentDescription"); + + /// + /// Generate a script that can be used on a database at any migration. + /// + public static string IdempotentDescription + => GetString("IdempotentDescription"); + + /// + /// Show JSON output. + /// + public static string JsonDescription + => GetString("JsonDescription"); + + /// + /// The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + /// + public static string MigrationDescription + => GetString("MigrationDescription"); + + /// + /// The starting migration. Defaults to '0' (the initial database). + /// + public static string MigrationFromDescription + => GetString("MigrationFromDescription"); + + /// + /// The name of the migration. + /// + public static string MigrationNameDescription + => GetString("MigrationNameDescription"); + + /// + /// Done. To undo this action, use 'ef migrations remove' + /// + public static string MigrationsAddCompleted + => GetString("MigrationsAddCompleted"); + + /// + /// Adds a new migration. + /// + public static string MigrationsAddDescription + => GetString("MigrationsAddDescription"); + + /// + /// Commands to manage migrations. + /// + public static string MigrationsDescription + => GetString("MigrationsDescription"); + + /// + /// Lists available migrations. + /// + public static string MigrationsListDescription + => GetString("MigrationsListDescription"); + + /// + /// The directory (and sub-namespace) to use. Paths are relative to the project directory. Defaults to "Migrations". + /// + public static string MigrationsOutputDirDescription + => GetString("MigrationsOutputDirDescription"); + + /// + /// Removes the last migration. + /// + public static string MigrationsRemoveDescription + => GetString("MigrationsRemoveDescription"); + + /// + /// Don't check to see if the migration has been applied to the database. + /// + public static string MigrationsRemoveForceDescription + => GetString("MigrationsRemoveForceDescription"); + + /// + /// Generates a SQL script from migrations. + /// + public static string MigrationsScriptDescription + => GetString("MigrationsScriptDescription"); + + /// + /// The ending migration. Defaults to the last migration. + /// + public static string MigrationToDescription + => GetString("MigrationToDescription"); + + /// + /// Missing required argument '{arg}'. + /// + public static string MissingArgument([CanBeNull] object arg) + => string.Format( + GetString("MissingArgument", nameof(arg)), + arg); + + /// + /// Missing required option '--{option}'. + /// + public static string MissingOption([CanBeNull] object option) + => string.Format( + GetString("MissingOption", nameof(option)), + option); + + /// + /// Don't use app domains. Always implied on .NET Core. + /// + public static string NoAppDomainDescription + => GetString("NoAppDomainDescription"); + + /// + /// Don't colorize output. + /// + public static string NoColorDescription + => GetString("NoColorDescription"); + + /// + /// No DbContext was found. + /// + public static string NoDbContext + => GetString("NoDbContext"); + + /// + /// No migrations were found. + /// + public static string NoMigrations + => GetString("NoMigrations"); + + /// + /// The file to write the result to. + /// + public static string OutputDescription + => GetString("OutputDescription"); + + /// + /// The directory to put files in. Paths are relative to the project directory. + /// + public static string OutputDirDescription + => GetString("OutputDirDescription"); + + /// + /// Prefix output with level. + /// + public static string PrefixDescription + => GetString("PrefixDescription"); + + /// + /// The project directory. Defaults to the current directory. + /// + public static string ProjectDirDescription + => GetString("ProjectDirDescription"); + + /// + /// The provider to use. (E.g. Microsoft.EntityFrameworkCore.SqlServer) + /// + public static string ProviderDescription + => GetString("ProviderDescription"); + + /// + /// The root namespace. Defaults to the target assembly name. + /// + public static string RootNamespaceDescription + => GetString("RootNamespaceDescription"); + + /// + /// The schemas of tables to generate entity types for. + /// + public static string SchemasDescription + => GetString("SchemasDescription"); + + /// + /// The startup assembly to use. Defaults to the target assembly. + /// + public static string StartupAssemblyDescription + => GetString("StartupAssemblyDescription"); + + /// + /// The tables to generate entity types for. + /// + public static string TablesDescription + => GetString("TablesDescription"); + + /// + /// Using application base '{appBase}'. + /// + public static string UsingApplicationBase([CanBeNull] object appBase) + => string.Format( + GetString("UsingApplicationBase", nameof(appBase)), + appBase); + + /// + /// Using assembly '{assembly}'. + /// + public static string UsingAssembly([CanBeNull] object assembly) + => string.Format( + GetString("UsingAssembly", nameof(assembly)), + assembly); + + /// + /// Using configuration file '{config}'. + /// + public static string UsingConfigurationFile([CanBeNull] object config) + => string.Format( + GetString("UsingConfigurationFile", nameof(config)), + config); + + /// + /// Using content root '{contentRoot}'. + /// + public static string UsingContentRoot([CanBeNull] object contentRoot) + => string.Format( + GetString("UsingContentRoot", nameof(contentRoot)), + contentRoot); + + /// + /// Using data directory '{dataDir}'. + /// + public static string UsingDataDir([CanBeNull] object dataDir) + => string.Format( + GetString("UsingDataDir", nameof(dataDir)), + dataDir); + + /// + /// Using project directory '{projectDir}'. + /// + public static string UsingProjectDir([CanBeNull] object projectDir) + => string.Format( + GetString("UsingProjectDir", nameof(projectDir)), + projectDir); + + /// + /// Using root namespace '{rootNamespace}'. + /// + public static string UsingRootNamespace([CanBeNull] object rootNamespace) + => string.Format( + GetString("UsingRootNamespace", nameof(rootNamespace)), + rootNamespace); + + /// + /// Using startup assembly '{startupAssembly}'. + /// + public static string UsingStartupAssembly([CanBeNull] object startupAssembly) + => string.Format( + GetString("UsingStartupAssembly", nameof(startupAssembly)), + startupAssembly); + + /// + /// Show verbose output. + /// + public static string VerboseDescription + => GetString("VerboseDescription"); + + /// + /// Writing '{file}'... + /// + public static string WritingFile([CanBeNull] object file) + => string.Format( + GetString("WritingFile", nameof(file)), + file); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} diff --git a/src/ef/Properties/Resources.Designer.tt b/src/ef/Properties/Resources.Designer.tt new file mode 100644 index 0000000000..2f70174a83 --- /dev/null +++ b/src/ef/Properties/Resources.Designer.tt @@ -0,0 +1,5 @@ +<# + Session["ResourceFile"] = "Resources.resx"; + Session["AccessModifier"] = "internal"; +#> +<#@ include file="..\..\..\tools\Resources.tt" #> \ No newline at end of file diff --git a/src/ef/Properties/Resources.resx b/src/ef/Properties/Resources.resx new file mode 100644 index 0000000000..c342dce4f6 --- /dev/null +++ b/src/ef/Properties/Resources.resx @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The assembly to use. + + + The connection string to the database. + + + The content root path. Defaults to the startup assembly directory. + + + The DbContext to use. + + + The name of the DbContext. + + + Use attributes to configure the model (where possible). If omitted, only the fluent API is used. + + + Commands to manage the database. + + + Drops the database. + + + This would drop the database '{database}' on server '{dataSource}'. + + + Show which database would be dropped, but don't drop it. + + + Don't confirm. + + + Are you sure you want to drop the database '{database}' on server '{dataSource}'? (y/N) + + + Database name: {database} + + + Updates the database to a specified migration. + + + The data directory. + + + Data source: {dataSource} + + + Commands to manage DbContext types. + + + Gets information about a DbContext type. + + + Lists available DbContext types. + + + Scaffolds a DbContext and entity types for a database. + + + Overwrite existing files. + + + Entity Framework Core Command Line Tools + + + The environment to use. Defaults to "Development". + + + Generate a script that can be used on a database at any migration. + + + Show JSON output. + + + The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + + + The starting migration. Defaults to '0' (the initial database). + + + The name of the migration. + + + Done. To undo this action, use 'ef migrations remove' + + + Adds a new migration. + + + Commands to manage migrations. + + + Lists available migrations. + + + The directory (and sub-namespace) to use. Paths are relative to the project directory. Defaults to "Migrations". + + + Removes the last migration. + + + Don't check to see if the migration has been applied to the database. + + + Generates a SQL script from migrations. + + + The ending migration. Defaults to the last migration. + + + Missing required argument '{arg}'. + + + Missing required option '--{option}'. + + + Don't use app domains. Always implied on .NET Core. + + + Don't colorize output. + + + No DbContext was found. + + + No migrations were found. + + + The file to write the result to. + + + The directory to put files in. Paths are relative to the project directory. + + + Prefix output with level. + + + The project directory. Defaults to the current directory. + + + The provider to use. (E.g. Microsoft.EntityFrameworkCore.SqlServer) + + + The root namespace. Defaults to the target assembly name. + + + The schemas of tables to generate entity types for. + + + The startup assembly to use. Defaults to the target assembly. + + + The tables to generate entity types for. + + + Using application base '{appBase}'. + + + Using assembly '{assembly}'. + + + Using configuration file '{config}'. + + + Using content root '{contentRoot}'. + + + Using data directory '{dataDir}'. + + + Using project directory '{projectDir}'. + + + Using root namespace '{rootNamespace}'. + + + Using startup assembly '{startupAssembly}'. + + + Show verbose output. + + + Writing '{file}'... + + \ No newline at end of file diff --git a/src/ef/ReflectionOperationExecutor.cs b/src/ef/ReflectionOperationExecutor.cs new file mode 100644 index 0000000000..e29f7dc7db --- /dev/null +++ b/src/ef/ReflectionOperationExecutor.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +#if NET451 +using System.IO; +#endif + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal class ReflectionOperationExecutor : OperationExecutorBase + { + private readonly object _executor; + private readonly Assembly _commandsAssembly; + private const string ReportHandlerTypeName = "Microsoft.EntityFrameworkCore.Design.OperationReportHandler"; + private const string ResultHandlerTypeName = "Microsoft.EntityFrameworkCore.Design.OperationResultHandler"; + private readonly Type _resultHandlerType; + + public ReflectionOperationExecutor( + string assembly, + string startupAssembly, + string projectDir, + string contentRootPath, + string dataDirectory, + string rootNamespace, + string environment) + : base(assembly, startupAssembly, projectDir, contentRootPath, dataDirectory, rootNamespace, environment) + { +#if NET451 + AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; +#endif + + _commandsAssembly = Assembly.Load(new AssemblyName { Name = DesignAssemblyName }); + var reportHandlerType = _commandsAssembly.GetType(ReportHandlerTypeName, throwOnError: true, ignoreCase: false); + + var reportHandler = Activator.CreateInstance( + reportHandlerType, + (Action)Reporter.WriteError, + (Action)Reporter.WriteWarning, + (Action)Reporter.WriteInformation, + (Action)Reporter.WriteVerbose); + + _executor = Activator.CreateInstance( + _commandsAssembly.GetType(ExecutorTypeName, throwOnError: true, ignoreCase: false), + reportHandler, + new Dictionary + { + { "targetName", AssemblyFileName }, + { "startupTargetName", StartupAssemblyFileName }, + { "projectDir", ProjectDirectory }, + { "contentRootPath", ContentRootPath }, + { "rootNamespace", RootNamespace }, + { "environment", EnvironmentName } + }); + + _resultHandlerType = _commandsAssembly.GetType(ResultHandlerTypeName, throwOnError: true, ignoreCase: false); + } + + protected override object CreateResultHandler() + => Activator.CreateInstance(_resultHandlerType); + + protected override void Execute(string operationName, object resultHandler, IDictionary arguments) + => Activator.CreateInstance( + _commandsAssembly.GetType(ExecutorTypeName + "+" + operationName, throwOnError: true, ignoreCase: true), + _executor, + resultHandler, + arguments); + +#if NET451 + private Assembly ResolveAssembly(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + foreach (var extension in new[] { ".dll", ".exe" }) + { + var path = Path.Combine(AppBasePath, assemblyName.Name + extension); + if (File.Exists(path)) + { + try + { + return Assembly.LoadFrom(path); + } + catch + { + } + } + } + + return null; + } + + public override void Dispose() + => AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly; +#endif + } +} diff --git a/src/ef/Reporter.cs b/src/ef/Reporter.cs new file mode 100644 index 0000000000..d433e9b69f --- /dev/null +++ b/src/ef/Reporter.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using static Microsoft.EntityFrameworkCore.Tools.AnsiConstants; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + internal static class Reporter + { + public static bool IsVerbose { get; set; } + public static bool NoColor { get; set; } + public static bool PrefixOutput { get; set; } + + public static string Colorize(string value, Func colorizeFunc) + => NoColor ? value : colorizeFunc(value); + + public static void WriteError(string message) + => WriteLine(Prefix("error: ", Colorize(message, x => Bold + Red + x + Reset))); + + public static void WriteWarning(string message) + => WriteLine(Prefix("warn: ", Colorize(message, x => Bold + Yellow + x + Reset))); + + public static void WriteInformation(string message) + => WriteLine(Prefix("info: ", message)); + + public static void WriteData(string message) + => WriteLine(Prefix("data: ", Colorize(message, x => Bold + Gray + x + Reset))); + + public static void WriteVerbose(string message) + { + if (IsVerbose) + { + WriteLine(Prefix("verbose: ", Colorize(message, x => Bold + Black + x + Reset))); + } + } + + private static string Prefix(string prefix, string value) + => PrefixOutput + ? string.Join( + Environment.NewLine, + value.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Select(l => prefix + l)) + : value; + + private static void WriteLine(string value) + { + if (NoColor) + { + Console.WriteLine(value); + } + else + { + AnsiConsole.WriteLine(value); + } + } + } +} diff --git a/src/ef/WrappedException.cs b/src/ef/WrappedException.cs new file mode 100644 index 0000000000..55b4012208 --- /dev/null +++ b/src/ef/WrappedException.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + public class WrappedException : Exception + { + private readonly string _stackTrace; + + public WrappedException(string type, string message, string stackTrace) + : base(message) + { + Type = type; + _stackTrace = stackTrace; + } + + public string Type { get; } + + public override string ToString() + => _stackTrace; + } +} diff --git a/src/ef/ef.csproj b/src/ef/ef.csproj new file mode 100644 index 0000000000..b782d63c31 --- /dev/null +++ b/src/ef/ef.csproj @@ -0,0 +1,50 @@ + + + + + + netcoreapp1.0;net451 + Entity Framework Core Command Line Tools + Exe + ef.x86 + false + Microsoft.EntityFrameworkCore.Tools + 1.0.0 + + + + + + + + + + + + + TextTemplatingFileGenerator + Resources.Designer.cs + + + + + + + + + + True + True + Resources.Designer.tt + + + + + + + + + + + + \ No newline at end of file diff --git a/test/dotnet-ef.Tests/CommandExceptionTest.cs b/test/dotnet-ef.Tests/CommandExceptionTest.cs new file mode 100644 index 0000000000..c8c4ba125f --- /dev/null +++ b/test/dotnet-ef.Tests/CommandExceptionTest.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + public class CommandExceptionTest + { + [Fact] + public void Ctor_works() + { + var ex = new CommandException("Message1"); + + Assert.Equal("Message1", ex.Message); + } + } +} diff --git a/test/dotnet-ef.Tests/CommandsTest.cs b/test/dotnet-ef.Tests/CommandsTest.cs new file mode 100644 index 0000000000..bdcbfbe17a --- /dev/null +++ b/test/dotnet-ef.Tests/CommandsTest.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.Cli.CommandLine; +using Xunit; + +using EFCommand = Microsoft.EntityFrameworkCore.Tools.Commands.RootCommand; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + public class CommandsTest + { + [Fact] + public void Short_names_are_unique() + { + foreach (var command in GetCommands()) + { + foreach (var group in command.Options.GroupBy(o => o.ShortName)) + { + Assert.True( + group.Key == null || group.Count() == 1, + "Duplicate short names on command '" + GetFullName(command) + "': " + + string.Join("; ", group.Select(o => o.Template))); + } + } + } + + private static IEnumerable GetCommands() + { + var app = new CommandLineApplication() + { + Name = "dotnet ef" + }; + + new EFCommand().Configure(app); + + return GetCommands(app); + } + + private static IEnumerable GetCommands(CommandLineApplication command) + { + var commands = new Stack(); + commands.Push(command); + + while (commands.Count != 0) + { + command = commands.Pop(); + + yield return command; + + foreach (var subcommand in command.Commands) + { + commands.Push(subcommand); + } + } + } + + private static string GetFullName(CommandLineApplication command) + { + var names = new Stack(); + + while (command != null) + { + names.Push(command.Name); + + command = command.Parent; + } + + return string.Join(" ", names); + } + } +} diff --git a/test/dotnet-ef.Tests/ExeTest.cs b/test/dotnet-ef.Tests/ExeTest.cs new file mode 100644 index 0000000000..f39700455e --- /dev/null +++ b/test/dotnet-ef.Tests/ExeTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Reflection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + public class ExeTest + { + [Fact] + public void ToArguments_works() + { + var result = ToArguments( + new[] { + "Good", + "Good\\", + "Needs quotes", + "Needs escaping\\", + "Needs escaping\\\\", + "Needs \"escaping\"", + "Needs \\\"escaping\"", + "Needs escaping\\\\too" + }); + + Assert.Equal( + "Good " + + "Good\\ " + + "\"Needs quotes\" " + + "\"Needs escaping\\\\\" " + + "\"Needs escaping\\\\\\\\\" " + + "\"Needs \\\"escaping\\\"\" " + + "\"Needs \\\\\\\"escaping\\\"\" " + + "\"Needs escaping\\\\\\\\too\"", + result); + } + + private static string ToArguments(IReadOnlyList args) + => (string)typeof(Exe).GetTypeInfo().GetMethod("ToArguments", BindingFlags.Static | BindingFlags.NonPublic) + .Invoke(null, new object[] { args }); + } +} diff --git a/test/dotnet-ef.Tests/dotnet-ef.Tests.csproj b/test/dotnet-ef.Tests/dotnet-ef.Tests.csproj new file mode 100644 index 0000000000..952ff1a646 --- /dev/null +++ b/test/dotnet-ef.Tests/dotnet-ef.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + netcoreapp1.0 + Microsoft.EntityFrameworkCore.Tools + + + + + + + + + + + \ No newline at end of file diff --git a/test/ef.Tests/AppDomainOperationExecutorTest.cs b/test/ef.Tests/AppDomainOperationExecutorTest.cs new file mode 100644 index 0000000000..90afd05678 --- /dev/null +++ b/test/ef.Tests/AppDomainOperationExecutorTest.cs @@ -0,0 +1,363 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET452 + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Relational.Design.Specification.Tests.TestUtilities; +using Microsoft.EntityFrameworkCore.Tools.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + [Collection("OperationExecutorTests")] + public class AppDomainOperationExecutorTest + { + private IOperationExecutor CreateExecutorFromBuildResult(BuildFileResult build, string rootNamespace = null) + => new AppDomainOperationExecutor(build.TargetPath, + build.TargetPath, + build.TargetDir, + build.TargetDir, + build.TargetDir, + rootNamespace, + environment: null); + + [Fact] + public void Assembly_load_errors_are_wrapped() + { + var targetDir = AppDomain.CurrentDomain.BaseDirectory; + using (var executor = new AppDomainOperationExecutor(Assembly.GetExecutingAssembly().Location, Path.Combine(targetDir, "Unknown.dll"), targetDir, null, null, null, null)) + { + Assert.Throws(() => executor.GetContextTypes()); + } + } + + [Fact] + public void GetMigrations_filters_by_context_name() + { + using (var directory = new TempDirectory()) + { + var targetDir = directory.Path; + var source = new BuildSource + { + TargetDir = targetDir, + References = + { + BuildReference.ByName("System.Diagnostics.DiagnosticSource", true), + BuildReference.ByName("System.Interactive.Async", true), + BuildReference.ByName("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + BuildReference.ByName("Microsoft.AspNetCore.Hosting.Abstractions", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Memory", true), + BuildReference.ByName("Microsoft.Extensions.Configuration.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.FileProviders.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Options", true), + BuildReference.ByName("Remotion.Linq", true) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Migrations; + + namespace MyProject + { + internal class Context1 : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(""Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SimpleProject.SimpleContext;Integrated Security=True""); + } + } + + internal class Context2 : DbContext + { + } + + namespace Migrations + { + namespace Context1Migrations + { + [DbContext(typeof(Context1))] + [Migration(""000000000000000_Context1Migration"")] + public class Context1Migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + + namespace Context2Migrations + { + [DbContext(typeof(Context2))] + [Migration(""000000000000000_Context2Migration"")] + public class Context2Migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + } + }" } + }; + + var build = source.Build(); + using (var executor = CreateExecutorFromBuildResult(build, "MyProject")) + { + var migrations = executor.GetMigrations("Context1"); + + Assert.Equal(1, migrations.Count()); + } + } + } + + [Fact] + public void GetContextType_works_with_multiple_assemblies() + { + using (var directory = new TempDirectory()) + { + var targetDir = directory.Path; + var contextsSource = new BuildSource + { + TargetDir = targetDir, + References = + { + BuildReference.ByName("Microsoft.EntityFrameworkCore", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Design", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + + namespace MyProject + { + public class Context1 : DbContext + { + } + + public class Context2 : DbContext + { + } + }" } + }; + var contextsBuild = contextsSource.Build(); + var migrationsSource = new BuildSource + { + TargetDir = targetDir, + References = + { + BuildReference.ByName("System.Reflection.Metadata", true), + BuildReference.ByName("Microsoft.AspNetCore.Hosting.Abstractions", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore"), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational.Design", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Configuration.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.FileProviders.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Options", true), + BuildReference.ByPath(contextsBuild.TargetPath) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Migrations; + + namespace MyProject + { + internal class Context3 : DbContext + { + } + + namespace Migrations + { + namespace Context1Migrations + { + [DbContext(typeof(Context1))] + [Migration(""000000000000000_Context1Migration"")] + public class Context1Migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + + namespace Context2Migrations + { + [DbContext(typeof(Context2))] + [Migration(""000000000000000_Context2Migration"")] + public class Context2Migration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + } + }" } + }; + var build = migrationsSource.Build(); + using (var executor = CreateExecutorFromBuildResult(build, "MyProject")) + { + var contextTypes = executor.GetContextTypes(); + + Assert.Equal(3, contextTypes.Count()); + } + } + } + + [Fact] + public void AddMigration_begins_new_namespace_when_foreign_migrations() + { + using (var directory = new TempDirectory()) + { + var targetDir = directory.Path; + var source = new BuildSource + { + TargetDir = targetDir, + References = + { + BuildReference.ByName("System.Diagnostics.DiagnosticSource", true), + BuildReference.ByName("System.Interactive.Async", true), + BuildReference.ByName("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + BuildReference.ByName("Microsoft.AspNetCore.Hosting.Abstractions", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Memory", true), + BuildReference.ByName("Microsoft.Extensions.Configuration.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.FileProviders.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Options", true), + BuildReference.ByName("Remotion.Linq", true) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Migrations; + + namespace MyProject + { + internal class MyFirstContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(""Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MyProject.MyFirstContext""); + } + } + + internal class MySecondContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(""Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MyProject.MySecondContext""); + } + } + + namespace Migrations + { + [DbContext(typeof(MyFirstContext))] + [Migration(""20151006140723_InitialCreate"")] + public class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + }" } + }; + var build = source.Build(); + using (var executor = CreateExecutorFromBuildResult(build, "MyProject")) + { + var artifacts = executor.AddMigration("MyMigration", /*outputDir:*/ null, "MySecondContext"); + Assert.Equal(3, artifacts.Keys.Count); + Assert.True(Directory.Exists(Path.Combine(targetDir, @"Migrations\MySecond"))); + } + } + } + + [Fact] + public void Throws_for_no_parameterless_constructor() + { + using (var directory = new TempDirectory()) + { + var targetDir = directory.Path; + var source = new BuildSource + { + TargetDir = targetDir, + References = + { + BuildReference.ByName("System.Diagnostics.DiagnosticSource", true), + BuildReference.ByName("System.Interactive.Async", true), + BuildReference.ByName("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + BuildReference.ByName("Microsoft.AspNetCore.Hosting.Abstractions", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Memory", true), + BuildReference.ByName("Microsoft.Extensions.Configuration.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.FileProviders.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Options", true), + BuildReference.ByName("Remotion.Linq", true) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Migrations; + + namespace MyProject + { + internal class MyContext : DbContext + { + public MyContext(DbContextOptions options) :base(options) {} + } + }" } + }; + var build = source.Build(); + using (var executor = CreateExecutorFromBuildResult(build, "MyProject")) + { + var ex = Assert.Throws( + () => executor.GetMigrations("MyContext")); + + Assert.Equal( + DesignStrings.NoParameterlessConstructor("MyContext"), + ex.Message); + } + } + } + } +} +#endif diff --git a/test/ef.Tests/CommandsTest.cs b/test/ef.Tests/CommandsTest.cs new file mode 100644 index 0000000000..43feb7d0e9 --- /dev/null +++ b/test/ef.Tests/CommandsTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Commands; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + public class CommandsTest + { + [Fact] + public void Short_names_are_unique() + { + foreach (var command in GetCommands()) + { + foreach (var group in command.Options.GroupBy(o => o.ShortName)) + { + Assert.True( + group.Key == null || group.Count() == 1, + "Duplicate short names on command '" + GetFullName(command) + "': " + + string.Join("; ", group.Select(o => o.Template))); + } + } + } + + private static IEnumerable GetCommands() + { + var app = new CommandLineApplication() + { + Name = "ef" + }; + + new RootCommand().Configure(app); + + return GetCommands(app); + } + + private static IEnumerable GetCommands(CommandLineApplication command) + { + var commands = new Stack(); + commands.Push(command); + + while (commands.Count != 0) + { + command = commands.Pop(); + + yield return command; + + foreach (var subcommand in command.Commands) + { + commands.Push(subcommand); + } + } + } + + private static string GetFullName(CommandLineApplication command) + { + var names = new Stack(); + + while (command != null) + { + names.Push(command.Name); + + command = command.Parent; + } + + return string.Join(" ", names); + } + } +} diff --git a/test/ef.Tests/SimpleProjectTest.cs b/test/ef.Tests/SimpleProjectTest.cs new file mode 100644 index 0000000000..6222e3b31c --- /dev/null +++ b/test/ef.Tests/SimpleProjectTest.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET452 + +using System; +using System.Collections; +using System.IO; +using System.Linq; +using Microsoft.EntityFrameworkCore.Relational.Design.Specification.Tests.TestUtilities; +using Microsoft.EntityFrameworkCore.Tools.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Tools +{ + [Collection("OperationExecutorTests")] + public class SimpleProjectTest : IClassFixture + { + private readonly SimpleProject _project; + + public SimpleProjectTest(SimpleProject project) + { + _project = project; + } + + private void AssertDefaultMigrationName(IDictionary artifacts) + => Assert.Contains("namespace SimpleProject.Migrations", File.ReadAllText(artifacts["MigrationFile"] as string)); + + [Fact] + public void AddMigration() + { + var artifacts = _project.Executor.AddMigration("EmptyMigration", "CustomFolder", "SimpleContext"); + Assert.NotNull(artifacts); + Assert.NotNull(artifacts["MigrationFile"]); + Assert.NotNull(artifacts["MetadataFile"]); + Assert.NotNull(artifacts["SnapshotFile"]); + Assert.True(Directory.Exists(Path.Combine(_project.TargetDir, "CustomFolder"))); + Assert.Contains("namespace SimpleProject.CustomFolder", File.ReadAllText(artifacts["MigrationFile"] as string)); + } + + [Fact] + public void AddMigration_output_dir_relative_to_projectdir() + { + var artifacts = _project.Executor.AddMigration("EmptyMigration1", "./CustomFolder", "SimpleContext"); + Assert.NotNull(artifacts); + Assert.StartsWith(Path.Combine(_project.TargetDir, "CustomFolder"), artifacts["MigrationFile"] as string); + Assert.Contains("namespace SimpleProject.CustomFolder", File.ReadAllText(artifacts["MigrationFile"] as string)); + } + + [Fact] + public void AddMigration_output_dir_relative_out_of_to_projectdir() + { + var artifacts = _project.Executor.AddMigration("EmptyMigration1", "../CustomFolder", "SimpleContext"); + Assert.NotNull(artifacts); + Assert.StartsWith(Path.GetFullPath(Path.Combine(_project.TargetDir, "../CustomFolder")), artifacts["MigrationFile"] as string); + AssertDefaultMigrationName(artifacts); + } + + [Fact] + public void AddMigration_output_dir_absolute_path_in_project() + { + var outputDir = Path.Combine(_project.TargetDir, "A/B/C"); + var artifacts = _project.Executor.AddMigration("EmptyMigration1", outputDir, "SimpleContext"); + Assert.NotNull(artifacts); + Assert.Equal(Path.Combine(outputDir, Path.GetFileName(artifacts["MigrationFile"] as string)), artifacts["MigrationFile"]); + Assert.Contains("namespace SimpleProject.A.B.C", File.ReadAllText(artifacts["MigrationFile"] as string)); + } + + [Fact] + public void AddMigration_output_dir_absolute_path_outside_project() + { + var outputDir = Path.GetTempPath(); + var artifacts = _project.Executor.AddMigration("EmptyMigration1", outputDir, "SimpleContext"); + Assert.NotNull(artifacts); + Assert.StartsWith(outputDir, artifacts["MigrationFile"] as string); + AssertDefaultMigrationName(artifacts); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void AddMigration_handles_empty_output_dir(string outputDir) + { + var artifacts = _project.Executor.AddMigration("EmptyMigration2", outputDir, "SimpleContext"); + Assert.NotNull(artifacts); + Assert.StartsWith(Path.Combine(_project.TargetDir, "Migrations"), artifacts["MigrationFile"] as string); + AssertDefaultMigrationName(artifacts); + } + + [Fact] + public void ScriptMigration() + { + var sql = _project.Executor.ScriptMigration(null, "InitialCreate", false, "SimpleContext"); + Assert.NotEmpty(sql); + } + + [Fact] + public void GetContextType() + { + var contextTypeName = _project.Executor.GetContextType("SimpleContext"); + Assert.StartsWith("SimpleProject.SimpleContext, ", contextTypeName); + } + + [Fact] + public void GetContextTypes() + { + var contextTypes = _project.Executor.GetContextTypes(); + Assert.Equal(1, contextTypes.Count()); + } + + [Fact] + public void GetMigrations() + { + var migrations = _project.Executor.GetMigrations("SimpleContext"); + Assert.Equal(1, migrations.Count()); + } + + [Fact] + public void GetContextInfo_returns_connection_string() + { + var info = _project.Executor.GetContextInfo("SimpleContext"); + Assert.Equal(@"(localdb)\MSSQLLocalDB", info["DataSource"]); + Assert.Equal("SimpleProject.SimpleContext", info["DatabaseName"]); + } + + public class SimpleProject : IDisposable + { + private readonly TempDirectory _directory = new TempDirectory(); + + public SimpleProject() + { + var source = new BuildSource + { + TargetDir = TargetDir, + References = + { + BuildReference.ByName("System.Diagnostics.DiagnosticSource", true), + BuildReference.ByName("System.Interactive.Async", true), + BuildReference.ByName("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"), + BuildReference.ByName("Microsoft.AspNetCore.Hosting.Abstractions", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational.Design", true), + BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Caching.Memory", true), + BuildReference.ByName("Microsoft.Extensions.Configuration.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection", true), + BuildReference.ByName("Microsoft.Extensions.DependencyInjection.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.FileProviders.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Logging", true), + BuildReference.ByName("Microsoft.Extensions.Logging.Abstractions", true), + BuildReference.ByName("Microsoft.Extensions.Options", true), + BuildReference.ByName("Remotion.Linq", true) + }, + Sources = { @" + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Migrations; + + namespace SimpleProject + { + internal class SimpleContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(""Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SimpleProject.SimpleContext;Integrated Security=True""); + } + } + + namespace Migrations + { + [DbContext(typeof(SimpleContext))] + [Migration(""20141010222726_InitialCreate"")] + public class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + } + } + } + }" } + }; + var build = source.Build(); + Executor = new AppDomainOperationExecutor(build.TargetPath, + build.TargetPath, + build.TargetDir, + build.TargetDir, + build.TargetDir, + "SimpleProject", + null); + } + + public string TargetDir => _directory.Path; + + internal IOperationExecutor Executor { get; } + + public void Dispose() + { + Executor.Dispose(); + _directory.Dispose(); + } + } + } +} +#endif \ No newline at end of file diff --git a/test/ef.Tests/TestUtilities/TempDirectory.cs b/test/ef.Tests/TestUtilities/TempDirectory.cs new file mode 100644 index 0000000000..51a10ba2f8 --- /dev/null +++ b/test/ef.Tests/TestUtilities/TempDirectory.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using IOPath = System.IO.Path; + +namespace Microsoft.EntityFrameworkCore.Tools.TestUtilities +{ + public class TempDirectory : IDisposable + { + public TempDirectory() + { + Path = IOPath.Combine(IOPath.GetTempPath(), IOPath.GetRandomFileName()); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + => Directory.Delete(Path, recursive: true); + } +} diff --git a/test/ef.Tests/ef.Tests.csproj b/test/ef.Tests/ef.Tests.csproj new file mode 100644 index 0000000000..123880e94b --- /dev/null +++ b/test/ef.Tests/ef.Tests.csproj @@ -0,0 +1,33 @@ + + + + + + net452;netcoreapp1.1 + netcoreapp1.1 + Microsoft.EntityFrameworkCore.Tools + + true + true + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/Resources.tt b/tools/Resources.tt index 352ca302d7..255ade2bc6 100644 --- a/tools/Resources.tt +++ b/tools/Resources.tt @@ -10,7 +10,7 @@ <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="EnvDTE" #> <# - var model = LoadResources((string)Session["ResourceFile"]); + var model = LoadResources(); #> // @@ -24,7 +24,7 @@ namespace <#= model.Namespace #> /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public static class <#= model.Class #> + <#= model.AccessModifier #> static class <#= model.Class #> { private static readonly ResourceManager _resourceManager = new ResourceManager("<#= model.ResourceName #>", typeof(<#= model.Class #>).GetTypeInfo().Assembly); @@ -76,13 +76,19 @@ namespace <#= model.Namespace #> } } <#+ - ResourceFile LoadResources(string resourceFile) + ResourceFile LoadResources() { var result = new ResourceFile(); + if (Session.ContainsKey("AccessModifier")) + { + result.AccessModifier = (string)Session["AccessModifier"]; + }; + var services = (IServiceProvider)Host; var dte = (DTE)services.GetService(typeof(DTE)); + var resourceFile = (string)Session["ResourceFile"]; if (!Path.IsPathRooted(resourceFile)) { resourceFile = Host.ResolvePath(resourceFile); @@ -139,6 +145,7 @@ namespace <#= model.Namespace #> class ResourceFile { public string Namespace { get; set; } + public string AccessModifier { get; set; } = "public"; public string Class { get; set; } public string ResourceName { get; set; } public IEnumerable Resources { get; set; }