EntityFramework Core 運行dotnet ef命令遷移背后本質是什么?(EF Core遷移原理)


前言

終於踏出第一步探索EF Core原理和本質,過程雖然比較漫長且枯燥乏味還得反復論證,其中滋味自知,EF Core的強大想必不用我再過多廢話,有時候我們是否思考過背后到底做了些什么,到底怎么實現的呢?比如本節要講的在命令行簡單敲下dotnet ef migrations add initial初始化表完事,如此簡潔。激起了我的好奇,下面我們來看看。本節內容可能比較多,請耐心。

EntityFramework Core命令基礎拾遺

我們提前創建好.NET Core Web應用程序和實體模型以及上下文,園中例子太多且我們也只是探討遷移原理,無關乎其他。

 

如此簡單一個命令就初始化了表,是不是很神奇,我們接下來要做的就是化神奇為簡單。我們接下來將上述遷移文件夾刪除,再次運行如下命令,看看遷移詳細過程。

dotnet ef migrations add init -c EFCoreDbContext -p ..\EfCore.Data\ --verbose

通過如上兩張圖我們可看出EF遷移將會進行兩步:第一步則是編譯上下文所在項目,編譯啟動項目。第二步則是通過編譯成功后的上下文所在程序集合啟動項目程序集最終實現遷移。總結起來就是簡單兩小步,背后所需要做的很多,請繼續往下看。 

EntityFramework Core遷移本質 

當我們敲寫dotnet ef migrations add initial命令后,緊接着會在啟動項目obj文件夾會生成如下文件。

這個東西是做什么的呢,我也不知道,我們打開該文件看看。

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="GetEFProjectMetadata" Condition="">
    <MSBuild Condition=" '$(TargetFramework)' == '' "
             Projects="$(MSBuildProjectFile)"
             Targets="GetEFProjectMetadata"
             Properties="TargetFramework=$(TargetFrameworks.Split(';')[0]);EFProjectMetadataFile=$(EFProjectMetadataFile)" />
    <ItemGroup Condition=" '$(TargetFramework)' != '' ">
      <EFProjectMetadata Include="AssemblyName: $(AssemblyName)" />
      <EFProjectMetadata Include="OutputPath: $(OutputPath)" />
      <EFProjectMetadata Include="Platform: $(Platform)" />
      <EFProjectMetadata Include="PlatformTarget: $(PlatformTarget)" />
      <EFProjectMetadata Include="ProjectAssetsFile: $(ProjectAssetsFile)" />
      <EFProjectMetadata Include="ProjectDir: $(ProjectDir)" />
      <EFProjectMetadata Include="RootNamespace: $(RootNamespace)" />
      <EFProjectMetadata Include="RuntimeFrameworkVersion: $(RuntimeFrameworkVersion)" />
      <EFProjectMetadata Include="TargetFileName: $(TargetFileName)" />
      <EFProjectMetadata Include="TargetFrameworkMoniker: $(TargetFrameworkMoniker)" />
    </ItemGroup>
    <WriteLinesToFile Condition=" '$(TargetFramework)' != '' "
                      File="$(EFProjectMetadataFile)"
                      Lines="@(EFProjectMetadata)" />
  </Target>
</Project>

一堆的如上東西,什么鬼玩意,剛看到這東西時我懵逼了,於是開始了探索之路。在.NET Core CLI 1.0.0有了稱為“項目工具擴展”的功能,我們稱之為“CLI工具”。 這些是項目特定的命令行工具,也就是說擴展了dotnet命令。比如我們安裝Microsoft.DotNet.Watcher.Tools包則可以使用dotnet watch命令,就是這么個意思。在.NET Core尚未完善時,項目文件采用JSON格式,緊接着改為了以擴展名為.xproj結尾的項目文件,格式也就轉換為了XML格式,最后項目文件定型為以.proj結尾,當然數據格式依然是XML,我猜測估計和MSBuild有關,因為微軟對XML數據格式的操作已經有非常成熟的庫,相比較而言JSON我們使用起來當然更加方便,可能微軟需要多做額外的工作,純屬猜測。了解和知道MSBuild的童鞋看到上述數據格式想必格外親切,再熟悉不過了,我們若仔細看到上述數據參數,就能夠明白上述參數是存放的項目參數。在.NET Core中都是利用MSBuild和CLI工具來讀取項目信息以用於其他目的。那么問題就來了,如何讀取項目信息呢?

利用MSBuild和CLI工具讀取項目信息

首先我們需要找到項目中以擴展名為.proj結尾的文件,其次我們需要注入MSBuild Target,最后則啟動進程是調用Target,代碼如下:

        public static void Main(string[] args)
        {

            var projectFile = @"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example\EFCore2Example.csproj";

            var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets";

            var projectExePath = Path.Combine(@"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example", "obj");

            Directory.CreateDirectory(projectExePath);

            var targetFile = Path.Combine(projectExePath, targetFileName);

            File.WriteAllText(targetFile,
 @"
<Project>
      <Target Name=""GetEFProjectMetadata"">
        <PropertyGroup>
            <EFProjectMetadata> 
AssemblyName: $(AssemblyName) 
OutputPath: $(OutputPath) 
Platform: $(Platform)
            </EFProjectMetadata>
        </PropertyGroup>
       <Message Importance=""High"" Text=""$(EFProjectMetadata)"" />
      </Target>
  </Project>");

            var psi = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo"
            };
            var process = Process.Start(psi);
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                Console.Error.WriteLine("Invoking MSBuild target failed");
            }
            Console.ReadKey();
        }

默認情況下MSBuildProjectExtensionsPath路徑在項目中obj文件夾下如上我們遷移的WebApplication1.csproj.EntityFrameworkCore.targets,我們對targets文件命名一般約定為$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).<SomethingUnique>.targets,如上代碼為我們仿照實際遷移時在obj文件夾下生成的targets文件。當啟動dotnet進程運行時會在控制台打印如下參數:

上述只是作為簡單的顯示信息而使用,利用CLI工具在我們項目內部創建了一個MSBuild目標。這個目標可以完成MSBuild所能做的任何事情,EF Core則是將加載目標讀取臨時文件的形式來獲取項目信息。

            var projectFile = @"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example\EFCore2Example.csproj";

            var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets";

            var projectExePath = Path.Combine(@"D:\Visual Studio 2015\Projects\EFCore2Example\EFCore2Example", "obj");

            Directory.CreateDirectory(projectExePath);

            var targetFile = Path.Combine(projectExePath, targetFileName);

            File.WriteAllText(targetFile,
 @"
<Project>
      <Target Name=""GetEFProjectMetadata"">
        <ItemGroup>
          <EFProjectMetadata Include = ""AssemblyName: $(AssemblyName)"" />
          <EFProjectMetadata Include = ""OutputPath: $(OutputPath)"" />
          <EFProjectMetadata Include = ""Platform: $(Platform)"" />
          <EFProjectMetadata Include = ""PlatformTarget: $(PlatformTarget)"" />
          <EFProjectMetadata Include = ""ProjectAssetsFile: $(ProjectAssetsFile)"" />
          <EFProjectMetadata Include = ""ProjectDir: $(ProjectDir)"" />
          <EFProjectMetadata Include = ""RootNamespace: $(RootNamespace)"" />
          <EFProjectMetadata Include = ""RuntimeFrameworkVersion: $(RuntimeFrameworkVersion)"" />
          <EFProjectMetadata Include = ""TargetFileName: $(TargetFileName)"" />
          <EFProjectMetadata Include = ""TargetFrameworkMoniker: $(TargetFrameworkMoniker)"" />
        </ItemGroup>
        <WriteLinesToFile
                              File =""$(EFProjectMetadataFile)""
                              Lines = ""@(EFProjectMetadata)"" />
      </Target>
  </Project>");


            var tmpFile = Path.GetTempFileName();
            var psi = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo \"/p:EFProjectMetadataFile={tmpFile}\""
            };
            var process = Process.Start(psi);
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                Console.Error.WriteLine("Invoking MSBuild target failed");
            }
            var lines = File.ReadAllLines(tmpFile);
            File.Delete(tmpFile);

            var properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach (var line in lines)
            {
                var idx = line.IndexOf(':');
                if (idx <= 0) continue;
                var name = line.Substring(0, idx)?.Trim();
                var value = line.Substring(idx + 1)?.Trim();
                properties.Add(name, value);
            }

            Console.WriteLine("........................................");

            Console.WriteLine($"EFCore2Example project has {properties.Count()}  properties");
            Console.WriteLine($"AssemblyName = { properties["AssemblyName"] }");
            Console.WriteLine($"OutputPath = { properties["OutputPath"] }");
            Console.WriteLine($"Platform = { properties["Platform"] }");
            Console.WriteLine($"PlatformTarget = { properties["PlatformTarget"] }");
            Console.WriteLine($"ProjectAssetsFile = { properties["ProjectAssetsFile"] }");
            Console.WriteLine($"ProjectDir = { properties["ProjectDir"] }");
            Console.WriteLine($"RootNamespace = { properties["RootNamespace"] }");
            Console.WriteLine($"RuntimeFrameworkVersion = { properties["RuntimeFrameworkVersion"] }");
            Console.WriteLine($"TargetFileName = { properties["TargetFileName"] }");
            Console.WriteLine($"TargetFrameworkMoniker = { properties["TargetFrameworkMoniker"] }");

            Console.WriteLine("........................................");        

上述是控制台中示例,若我們在.NET Core  Web應用程序中,此時我們完全可以獲取到項目文件而無需如控制台寫死項目文件路徑,如下:

             var projectFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly)
                   .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase))
                   .Take(2).ToList();

            var projectFile = projectFiles[0];

            var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets";
            .......

此時獲取到啟動項目信息,如下:

到了這里我們探索完了EF Core如何進行遷移的第一步,同時我們也明白為何要將執行命令路徑切換到啟動項目項目文件所在目錄,因為需要獲取到項目信息,然后進行Build也就是生成,如果執行生成錯誤則返回,否則返回項目詳細信息。到這里我們了解了利用MSBuild和CLI工具來獲取上下文所在項目詳細信息和啟動項目詳細信息。我們繼續往下探討。

執行.NET Core必需文件和調用ef.exe或者ef.x86.exe應用程序或者ef.dll程序集執行遷移

通過上述MSBuild和CLI工具我們獲取到上下文和啟動項目詳細信息,接下來則是進行遷移,如開頭第四張圖片所示,所執行命令大致如下:

dotnet exec --depsfile [.deps.json] --addtionalprobingpath [nugetpackage] --runtimeconfig [.runtimeconfig.json] ef.dll migrations add 
init -c [DbContext] --assembly [DbContextAssmbly] --startup-assembly [StartupProjectAssembly]

一波剛平息 一波又起,首先我們得明白上述命令,比如通過讀取擴展名為.deps.json文件來執行--depsfile命令,以及讀取擴展名為.runtimeconfig.json文件執行--runtimeconfig命令,那么這兩個文件是做什么的呢,我們又得花費一點功夫來講解。接下來我們利用dotnet命令來創建控制台程序來初識上述兩個命令的作用。首先我們運行如下命令創建控制台程序,在此需要特別說明的是在.NET Core  2.0后當通過dotnet  build后直接包含了執行dotnet restore命令

dotnet new Console

此時同時也會在obj文件夾下生成project.assets.json文件,這個文件是做什么的呢?別着急,我們先講完.deps.json和.runtimeconfig.json繼續話題會講到這個文件的作用,我們繼續。

此時我們繼續運行生成命令,如下則會生成bin文件夾,同時在如下.netcoreapp2.1文件夾會生成我們需要講到的兩個json文件。

dotnet build

 

{
  "runtimeOptions": {
    "tfm": "netcoreapp2.1",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "2.1.0-preview1-26216-03"
    }
  }
}

運行.NET Core應用程序必須要runtimeconfig.json文件,意為“運行時”,我們也可以翻譯為共享框架,且運行時和共享框架概念可任意轉換。此json文件為運行時配置選項,如果沒有runtimeconfig.json文件,將拋出異常,我們刪除該文件看看。

通過運行時json文件當運行時指示dotnet運行Microsoft.NETCore.App 2.0.0共享框架 此框架是最常用的框架,但也存在其他框架,例如Microsoft.AspNetCore.App。 與.NET Framework不同,可能會在計算機上安裝多個.NET Core共享框架。dotnet讀取json文件,並在C:\Program Files\dotnet\shared中查找運行該應用程序所需的文件,如下存在多個運行時框架。當然如果我們安裝了更高版本的.net core如2.1.0-preview1-final,此時dotnet將自動選擇最高的版本。

好了,我們算是明白.runtimeconfig.json文件主要是用來指示dotnet在運行時使用哪個框架。我們再來看看.deps.json文件,如下:

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v2.1",
    "signature": "da39a3ee5e6b4b0d3255bfef95601890afd80709"
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v2.1": {
      "認識.NET Core/1.0.0": {
        "runtime": {
          "認識.NET Core.dll": {}
        }
      }
    }
  },
  "libraries": {
    "認識.NET Core/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}

deps.json文件是一個依賴關系清單。它可以用來配置來自包的組件的動態鏈接。NET Core可以配置為從多個位置動態加載程序集,這些位置包括:

應用程序基目錄(與入口點應用程序位於同一文件夾中,不需要配置)

  1. 包緩存文件夾(NuGet恢復緩存或NuGet后備文件夾)
  2. 優化的包緩存或運行時包存儲。
  3. 共享框架(通過runtimeconfig.json配置)。

好了,對於.deps.json和runtimeconfig.json文件暫時先講到這里,后續有可能再詳細講解,我們弄明白了這兩個文件的大致作用即可。回到我們的話題,那么這兩個文件是如何找到的呢?那就得結合我們第一步獲取到的項目信息了,在第一部分獲取項目信息最后給出的圖片里面根據ProjectDir和OutputPath就可以獲取到.deps.json和.runtimeconfig.json文件。最后則需要獲取ef.dll程序集從而執行相關遷移命令,那么ef.dll程序集是怎么獲取到的呢?這個時候就需要獲取項目中的信息ProjectAssetsFile即讀取project.assets.json文件,獲取packageFolders節點下數據,如下:

 "packageFolders": {
    "C:\\Users\\JeffckyWang\\.nuget\\packages\\": {},
    "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackagesFallback\\": {},
    "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder": {}
  },

我們從開頭第四張圖片可看出對於--addtionalprobingpath有三個路徑也就是如上三個路徑,我們看看如上三個路徑是否存在ef.dll程序集。

 

如上只有nuget和sdk中有ef.dll程序集,我們依然看看開頭第四張圖片最終執行的卻是sdk中的ef.dll程序集,難道是如果nuget和skd目錄在project.assets.json中都存在,那么優先從sdk中查找么,也就是sdk中程序集優先級比nuget程序集高嗎,如果sdk中存在對應程序集則直接執行嗎。當移除該文件中nuget路徑,重新生成會覆蓋。所以猜測可能優先查找sdk中是否存在ef.dll程序集。這里還需額外說明一點的是我們在第一節獲取到了項目詳細信息,其中有一項是TargetFrameworkMoniker,若我們創建的項目是.NET Framework,此時根據TargetFrameworkMoniker來判斷,若為.NETCoreApp則執行上述ef.dll程序集否則執行如下路徑應用程序來遷移。

手動執行命令遷移 

上述我們完整講述了在命令行中執行dotnet ef命令背后的本質是什么,那么我們接下來利用代碼手動來遷移。如下第一個類為解析進程所需的參數類【從dotnet ef源碼拷貝而來】

    public static class Common
    {
        public static string ToArguments(IReadOnlyList<string> 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();
        }
    }

項目所需的詳細信息,我們封裝成一個類且其中包含執行build命令的方法,如下:

    public class Project
    {
        public string AssemblyName { get; set; }
        public string Language { 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 RuntimeFrameworkVersion { get; set; }
        public string TargetFileName { get; set; }
        public string TargetFrameworkMoniker { get; set; }

        public void Build()
        {
            var args = new List<string>
            {
                "build"
            };

            args.Add("/p:GenerateRuntimeConfigurationFiles=True");
            args.Add("/verbosity:quiet");
            args.Add("/nologo");

            var arg = Common.ToArguments(args);

            var psi = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = arg
            };
            var process = Process.Start(psi);
            process.WaitForExit();
        }
    }

接下來則是獲取項目詳細信息、生成、遷移,如下三個方法以及對應方法實現。

            //獲取項目詳細信息
            var projectMedata = GetProjectMedata();

            //生成
            projectMedata.Build();

            //執行EF遷移命令
            ExecuteEFCommand(projectMedata);
        public Project GetProjectMedata()
        {
            var projectFiles = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly)
                  .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase))
                  .Take(2).ToList();

            var projectFile = projectFiles[0];

            var targetFileName = Path.GetFileName(projectFile) + ".EntityFrameworkCore.targets";

            var projectExePath = Path.Combine(Path.GetDirectoryName(projectFile), "obj");

            Directory.CreateDirectory(projectExePath);

            var targetFile = Path.Combine(projectExePath, targetFileName);

            System.IO.File.WriteAllText(targetFile,
 @"
<Project>
      <Target Name=""GetEFProjectMetadata"">
        <ItemGroup>
          <EFProjectMetadata Include = ""AssemblyName: $(AssemblyName)"" />
          <EFProjectMetadata Include = ""OutputPath: $(OutputPath)"" />
          <EFProjectMetadata Include = ""Platform: $(Platform)"" />
          <EFProjectMetadata Include = ""PlatformTarget: $(PlatformTarget)"" />
          <EFProjectMetadata Include = ""ProjectAssetsFile: $(ProjectAssetsFile)"" />
          <EFProjectMetadata Include = ""ProjectDir: $(ProjectDir)"" />
          <EFProjectMetadata Include = ""RootNamespace: $(RootNamespace)"" />
          <EFProjectMetadata Include = ""RuntimeFrameworkVersion: $(RuntimeFrameworkVersion)"" />
          <EFProjectMetadata Include = ""TargetFileName: $(TargetFileName)"" />
          <EFProjectMetadata Include = ""TargetFrameworkMoniker: $(TargetFrameworkMoniker)"" />
        </ItemGroup>
        <WriteLinesToFile
                              File =""$(EFProjectMetadataFile)""
                              Lines = ""@(EFProjectMetadata)"" Overwrite=""true"" />
      </Target>
  </Project>");


            var tmpFile = Path.GetTempFileName();
            var psi = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = $"msbuild \"{projectFile}\" /t:GetEFProjectMetadata /nologo \"/p:EFProjectMetadataFile={tmpFile}\""
            };
            var process = Process.Start(psi);
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                Console.Error.WriteLine("Invoking MSBuild target failed");
            }
            var lines = System.IO.File.ReadAllLines(tmpFile);
            System.IO.File.Delete(tmpFile);

            var properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach (var line in lines)
            {
                var idx = line.IndexOf(':');
                if (idx <= 0) continue;
                var name = line.Substring(0, idx)?.Trim();
                var value = line.Substring(idx + 1)?.Trim();
                properties.Add(name, value);
            }

            var project = new Project()
            {
                AssemblyName = properties["AssemblyName"],
                OutputPath = properties["OutputPath"],
                ProjectDir = properties["ProjectDir"],
                ProjectAssetsFile = properties["ProjectAssetsFile"],
                TargetFileName = properties["TargetFileName"],
                TargetFrameworkMoniker = properties["TargetFrameworkMoniker"],
                RuntimeFrameworkVersion = properties["RuntimeFrameworkVersion"],
                PlatformTarget = properties["PlatformTarget"],
                RootNamespace = properties["RootNamespace"]
            };
            return project;
        }
        public void ExecuteEFCommand(Project project)
        {var depsFile = Path.Combine(
                project.ProjectDir,
                project.OutputPath,
                project.AssemblyName + ".deps.json");
            var runtimeConfig = Path.Combine(
                project.ProjectDir,
                 project.OutputPath,
                project.AssemblyName + ".runtimeconfig.json");
            var projectAssetsFile = project.ProjectAssetsFile;

            var args = new List<string>
            {
                "exec",
                "--depsfile"
            };
            args.Add(depsFile);

            var packageSDKFolder = string.Empty;
            if (!string.IsNullOrEmpty(projectAssetsFile))
            {
                using (var reader = new JsonTextReader(System.IO.File.OpenText(projectAssetsFile)))
                {
                    var projectAssets = JToken.ReadFrom(reader);
                    var packageFolders = projectAssets["packageFolders"].Children<JProperty>().Select(p => p.Name);
                    foreach (var packageFolder in packageFolders)
                    {
                        packageSDKFolder = packageFolder;
                        args.Add("--additionalprobingpath");
                        args.Add(packageFolder.TrimEnd(Path.DirectorySeparatorChar));
                    }
                }
            }
            if (System.IO.File.Exists(runtimeConfig))
            {
                args.Add("--runtimeconfig");
                args.Add(runtimeConfig);
            }
            else if (project.RuntimeFrameworkVersion.Length != 0)
            {
                args.Add("--fx-version");
                args.Add(project.RuntimeFrameworkVersion);
            }

            args.Add(Path.Combine(@"C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.entityframeworkcore.tools.dotnet\2.0.2\tools\netcoreapp2.0", "ef.dll"));

            args.AddRange(new List<string>() { "migrations", "add", "initial", "-c", "EFCoreDbContext" });
            args.Add("--assembly");
            args.Add(Path.Combine(project.ProjectDir, project.OutputPath, project.TargetFileName));
            args.Add("--startup-assembly");
            args.Add(Path.Combine(project.ProjectDir, project.OutputPath, project.TargetFileName));if (!string.IsNullOrEmpty(project.Language))
            {
                args.Add("--language");
                args.Add(project.Language);
            }

            var arg = Common.ToArguments(args);
            var psi = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = arg,
                UseShellExecute = false
            };
            var process = Process.Start(psi);
            process.WaitForExit();
            if (process.ExitCode != 0)
            {
                Console.WriteLine("Migration failed");
            }
        }

請注意在上述ExecuteEFCommand方法中已明確標注此時目標遷移目錄就是上述當前項目,需要遷移到上下文所在類庫中,我們在命令行就可以得到上下文所在項目,此時只需要將上述ExecuteEFCommand方法中標注改為從命令行獲取到的項目參數即可,如下我們直接寫死:

 args.Add("--assembly");
 args.Add(Path.Combine(project.ProjectDir, project.OutputPath, "EFCore.Data.dll"));

同時還需添加上下文項目目錄參數,如下:

args.Add("--project-dir");            
args.Add(@"C:\Users\JeffckyWang\Source\Repos\WebApplication1\EFCore.Data\"); if (!string.IsNullOrEmpty(project.Language)) { args.Add("--language"); args.Add(project.Language); } .......

最后將啟動項目中生成的遷移目錄修改為上下文所在項目,如下:

            var sqlStr = @"data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=EFCore2xDb;
          integrated security=True;MultipleActiveResultSets=True;
"; services.AddDbContextPool<EFCoreDbContext>(options => { options.UseSqlServer(sqlStr, d => d.MigrationsAssembly("EFCore.Data")); }, 256);

此時我們再來手動遷移那么將在上下文所在項目中生成遷移文件夾,如下:

了解了執行dotnet ef背后實現的原理,Jeff要說了【那么問題來了】,對於我們而言有何幫助沒有呢,當然有而且馬上能實現,我們可以寫一個批處理文件在發布時直接執行生成數據庫表,說完就開干。我們在上述WebApplication1啟動項目中創建名為deploy-efcore.bat批處理文件,代碼如下:

set EFCoreMigrationsNamespace=%WebApplication1
set EFCoreMigrationsDllName=%WebApplication1.dll
set EFCoreMigrationsDllDepsJson=%bin\debug\netcoreapp2.0\WebApplication1.deps.json
set PathToNuGetPackages=%USERPROFILE%\.nuget\packages set PathToEfDll=%PathToNuGetPackages%\microsoft.entityframeworkcore.tools.dotnet\2.0.0\tools\netcoreapp2.0\ef.dll dotnet exec --depsfile .\%EFCoreMigrationsDllDepsJson% --additionalprobingpath %PathToNuGetPackages% %PathToEfDll% database update --assembly .\%EFCoreMigrationsDllName% --startup-assembly .\%EFCoreMigrationsDllName% --project-dir . --verbose --root-namespace %EFCoreMigrationsNamespace% pause

總結 

本節我們詳細講解了執行dotnet ef命令背后究竟發生了什么,同時也大概討論了下.NET Core幾個配置文件的作用,足夠了解這些,當出現問題才不至於手足無措,耗時一天多才寫完,不過收獲頗多,下面我們給出背后實現大致原理【后面可能會更詳細探討,到時繼續更新】圖來解釋執行dotnet ef命令背后的本質以此來加深印象,希望對閱讀的您也能有所幫助,我們下節再會。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM