﻿# 如何使用 .NET CLI 快速產生 ASP․NET Core 的 Controllers 與 Views 程式碼

我們在 Visual Studio 2019 開發 ASP․NET Core 的時候，都可以透過 \[加入\] / \[控制器\] 的方式，很便利的快速產生 Controllers 與 Views 程式碼，如果已經有現成的 Models 模型類別存在，還能快速的完成 CRUD 等實作。可惜在 Visual Studio Code 裡面，似乎就沒有相對應的工具可用，這對使用 macOS 或 Linux 的開發者來說，就顯得有點不太方便。不過，微軟官方其實有提供一套 `dotnet-aspnet-codegenerator` .NET CLI 全域工具，所做的事情跟 Visual Studio 2019 在做的事情完全一樣，本篇文章就來說說這套工具的用法，以及常見的地雷與注意事項。

### 建立範例 Web API 專案

1.  建立 `api1` 專案
    
    ```bash
    dotnet new webapi -n api1 && cd api1
    ```
    
2.  安裝 [dotnet-ignore](https://www.nuget.org/packages/dotnet-ignore) 全域工具 (.NET CLI Global Tool)
    
    ```bash
    dotnet tool update -g dotnet-ignore
    ```
    
3.  加入 `.gitignore` 檔案
    
    ```bash
    dotnet ignore get -n visualstudio
    ```
    
4.  將專案加入 Git 版控
    
    ```bash
    git init && git add . && git commit -m "Initial commit"
    ```
    
5.  在資料庫中建立 [ContosoUniversity](https://gist.githubusercontent.com/doggy8088/2a2f7075d49b3814d19513426ede3549/raw/ab95b323425ff98e99b901793d0361d90439fb0b/ContosoUniversity.sql) 範例資料庫
    
    建議使用 SQL Server Express LocalDB 即可：`(localdb)\MSSQLLocalDB`
    
6.  安裝 `dotnet-ef` 全域工具 (.NET CLI Global Tool)
    
    ```bash
    dotnet tool update -g dotnet-ef
    ```
    
7.  安裝 Entity Framework Core 相關套件
    
    ```bash
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Design
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    dotnet add package Microsoft.EntityFrameworkCore.Sqlite
    dotnet add package Microsoft.EntityFrameworkCore.InMemory
    ```
    
    建立新版本
    
    ```bash
    git add . && git commit -m "Add EFCore NuGet packages"
    ```
    
8.  透過 `dotnet-ef` 快速建立 EFCore 模型類別與資料內容類別
    
    ```bash
    dotnet ef dbcontext scaffold "Server=(localdb)\MSSQLLocalDB;Database=ContosoUniversity;Trusted_Connection=True;MultipleActiveResultSets=true" Microsoft.EntityFrameworkCore.SqlServer -o Models
    dotnet build
    ```
    
    在透過 `dotnet ef` 產生程式碼之後，必須先建置專案，雖然建置會成功，但是卻會看到**警告訊息**如下：
    
    ![warning CS1030: #warning: 'To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.'](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f6ae53886ed3fe5507bc-92549573-29de5000-f28c-11ea-82cb-d064b0503bbc.jpg)
    
    ```
    Models\ContosoUniversityContext.cs(32,10): warning CS1030: #warning: 'To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.' [G:\Projects\api1\api1.csproj]
    ```
    
    這個警告訊息主要是跟你說，在透過 `dotnet-ef` 工具產生程式碼的時候，會順便將「連接字串」一定產生在 `ContosoUniversityContext.cs` 原始碼中，建議你將這段程式碼移除。
    
    建立新版本
    
    ```bash
    git add . && git commit -m "Create EFCore models and dbcontext classes using dotnet-ef"
    ```
    
9.  調整 ASP․NET Core 的 `Startup` 類別並對 `ContosoUniversityContext` 設定 DI
    
    ```csharp
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ContosoUniversityContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddControllers();
    }
    ```
    
10.  調整 ASP․NET Core 的 `appsettings.json` 加入 `DefaultConnection` 連接字串
     
     ```json
     {
       "ConnectionStrings": {
           "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Initial Catalog=ContosoUniversity;Integrated Security=True"
       },
       "Logging": {
         "LogLevel": {
           "Default": "Information",
           "Microsoft": "Warning",
           "Microsoft.Hosting.Lifetime": "Information"
         }
       },
       "AllowedHosts": "*"
     }
     ```
     
     建立新版本
     
     ```bash
     git add . && git commit -m "Add DI for ContosoUniversityContext and configure ConnectionStrings"
     ```
     
11.  建置專案，確認可以正常編譯！
     
     ```bash
     dotnet build
     ```
     

### 透過現有的 Models 快速建立 API 控制器

在擁有 EFCore 的所有 Models 類別之後，就可以開始利用 `dotnet aspnet-codegenerator` 命令，快速產生專案所需的 Controllers 或 Views，我們立馬來看看如何使用這個工具。

1.  先執行 `dotnet aspnet-codegenerator` 命令，查看用法
    
    ```
    G:\Projects\api1>dotnet aspnet-codegenerator
    
    
    Usage: aspnet-codegenerator [arguments] [options]
    
    Arguments:
      generator  Name of the generator. Check available generators below.
    
    Options:
      -p|--project             Path to .csproj file in the project.
      -n|--nuget-package-dir
      -c|--configuration       Configuration for the project (Possible values: Debug/ Release)
      -tfm|--target-framework  Target Framework to use. (Short folder name of the tfm. eg. net46)
      -b|--build-base-path
      --no-build
    
    No code generators are available in this project.Add Microsoft.VisualStudio.Web.CodeGeneration.Design package to the project as a NuGet package reference.
    
    RunTime 00:00:02.02
    ```
    
    這段說明訊息的重點在這裡：
    
    ```
    No code generators are available in this project. Add Microsoft.VisualStudio.Web.CodeGeneration.Design package to the project as a NuGet package reference.
    ```
    
    簡單來說，你需要額外安裝 `Microsoft.VisualStudio.Web.CodeGeneration.Design` 套件，才可以使用這套工具。
    
    > 就算你使用 Visual Studio 2019 來產生 Controllers 或 Views，其實背後也是幫你安裝相同的套件！
    
    請用以下命令安裝：
    
    ```bash
    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    ```
    
    建立新版本
    
    ```bash
    git add . && git commit -m "dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design"
    ```
    
2.  再執行一次 `dotnet aspnet-codegenerator` 命令，查看用法
    
    如果你可以看到 `Available generators` 的資訊，就代表環境已經準備好。你可以透過這套工具，快速的產生 MVC Area, MVC Views, Controller, MVC Area with controllers and views, RazorPage 等等。
    
    ```
    G:\Projects\api1>dotnet aspnet-codegenerator
    
    
    Usage: aspnet-codegenerator [arguments] [options]
    
    Arguments:
      generator  Name of the generator. Check available generators below.
    
    Options:
      -p|--project             Path to .csproj file in the project.
      -n|--nuget-package-dir
      -c|--configuration       Configuration for the project (Possible values: Debug/ Release)
      -tfm|--target-framework  Target Framework to use. (Short folder name of the tfm. eg. net46)
      -b|--build-base-path
      --no-build
    
    Available generators:
      area      : Generates an MVC Area.
      controller: Generates a controller.
      identity  : Generates an MVC Area with controllers and
      razorpage : Generates RazorPage(s).
      view      : Generates a view.
    
    
    RunTime 00:00:01.68
    ```
    
3.  示範如何快速建立 `CoursesController` 控制器（沒有 Views 頁面）
    
    請先嘗試執行以下命令：
    
    ```bash
    dotnet aspnet-codegenerator controller -name CoursesController -async -api -m Course -dc ContosouniversityContext -outDir Controllers
    ```
    
    個別參數說明請自行參考 `dotnet aspnet-codegenerator controller --help` 說明文件。
    
    ```
    Usage: aspnet-codegenerator [arguments] [options]
    
    Arguments:
      generator  Name of the generator. Check available generators below.
    
    Options:
      -p|--project             Path to .csproj file in the project.
      -n|--nuget-package-dir
      -c|--configuration       Configuration for the project (Possible values: Debug/ Release)
      -tfm|--target-framework  Target Framework to use. (Short folder name of the tfm. eg. net46)
      -b|--build-base-path
      --no-build
    
    Selected Code Generator: controller
    
    Generator Options:
      --controllerName|-name              : Name of the controller
      --useAsyncActions|-async            : Switch to indicate whether to generate async controller actions
      --noViews|-nv                       : Switch to indicate whether to generate CRUD views
      --restWithNoViews|-api              : Specify this switch to generate a Controller with REST style API, noViews is assumed and any view related options are ignored
      --readWriteActions|-actions         : Specify this switch to generate Controller with read/write actions when a Model class is not used
      --model|-m                          : Model class to use
      --dataContext|-dc                   : DbContext class to use
      --referenceScriptLibraries|-scripts : Switch to specify whether to reference script libraries in the generated views
      --layout|-l                         : Custom Layout page to use
      --useDefaultLayout|-udl             : Switch to specify that default layout should be used for the views
      --force|-f                          : Use this option to overwrite existing files
      --relativeFolderPath|-outDir        : Specify the relative output folder path from project where the file needs to be generated, if not specified, file will be generated in the project folder
      --controllerNamespace|-namespace    : Specify the name of the namespace to use for the generated controller
      --useSqlite|-sqlite                 : Flag to specify if DbContext should use SQLite instead of SQL Server.
    ```
    
    這段命令其實會產生以下錯誤：
    
    ```
    Building project ...
    Finding the generator 'controller'...
    Running the generator 'controller'...
    Generating a new DbContext class 'ContosouniversityContext'
    Attempting to compile the application in memory with the added DbContext.
    Attempting to figure out the EntityFramework metadata for the model and DbContext: 'Course'
    The entity type 'CourseInstructor' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'. StackTrace:
      at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateNonNullPrimaryKeys(IModel model, IDiagnosticsLogger`1 logger)
      at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
      at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
      at Microsoft.EntityFrameworkCore.SqlServer.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
      at Microsoft.EntityFrameworkCore.Metadata.Conventions.ValidatingConvention.ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
      at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelFinalized(IConventionModelBuilder modelBuilder)
      at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelFinalized(IConventionModelBuilder modelBuilder)
      at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FinalizeModel()
      at Microsoft.EntityFrameworkCore.ModelBuilder.FinalizeModel()
      at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
      at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
      at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
      at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
      at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_3(IServiceProvider p)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
    The entity type 'CourseInstructor' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
      at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
      at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
      at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
      at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
      at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
      at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
      at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
      at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
      at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func`1 factory)
      at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
      at Microsoft.EntityFrameworkCore.Design.DbContextActivator.CreateInstance(Type contextType, Assembly startupAssembly, IOperationReportHandler reportHandler)
      at Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore.EntityFrameworkModelProcessor.TryCreateContextUsingAppCode(Type dbContextType, Type startupType)
    
      at Microsoft.VisualStudio.Web.CodeGeneration.ActionInvoker.<BuildCommandLine>b__6_0()
      at Microsoft.Extensions.CommandLineUtils.CommandLineApplication.Execute(String[] args)
      at Microsoft.VisualStudio.Web.CodeGeneration.ActionInvoker.Execute(String[] args)
      at Microsoft.VisualStudio.Web.CodeGeneration.CodeGenCommand.Execute(String[] args)
    ```
    
    這個錯誤其實非常難以理解，而且會誤導偵錯方向。
    
    事實上，只是**資料內容類別**的名稱打錯而已 (也就是 `-dc` 參數)，而且執行錯誤後，你原本的 `appsettings.json` 也會被多新增一個連接字串名為 `ContosouniversityContext` (打錯的名稱)，這個是多餘的，應該手動刪除多餘的連接字串。
    
    ![diff --git a/appsettings.json b/appsettings.json](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/3f2aeac9d936a7be5e17-92551768-72e4d300-f291-11ea-9c93-e5bac76377e4.jpg)
    
    這點真的**非常地雷**，各位務必要特別注意類別名稱大小寫必須正確輸入！
    
    正確：`ContosoUniversityContext`
    
    錯誤：`ContosouniversityContext`
    
    正確的命令如下：
    
    ```bash
    dotnet aspnet-codegenerator controller -name CoursesController -async -api -m Course -dc ContosoUniversityContext -outDir Controllers
    ```
    
    ```
    Building project ...
    Finding the generator 'controller'...
    Running the generator 'controller'...
    Attempting to compile the application in memory.
    Attempting to figure out the EntityFramework metadata for the model and DbContext: 'Course'
    Added Controller : '\Controllers\CoursesController.cs'.
    RunTime 00:00:10.15
    ```
    
    如此一來，完整的 `CoursesController` 就自動產生了！ 👍
    
4.  示範如何快速建立 `DepartmentsController` 控制器（包含 Views 頁面與使用現有 Layout 版面）
    
    請執行以下命令：
    
    ```bash
    dotnet aspnet-codegenerator controller -name DepartmentsController -async -scripts -udl -m Department -dc ContosoUniversityContext -outDir Controllers
    ```
    
    > 這裡的 `-udl` 是 **Use Default Layout** 的意思，如果你的專案有現成的 Layout 可用，請務必加上這個參數選項。
    
5.  示範如何快速建立 `DepartmentsController` 控制器（包含 Views 頁面與不使用任何 Layout 版面）
    
    請執行以下命令：
    
    ```bash
    dotnet aspnet-codegenerator controller -name DepartmentsController -async -scripts -m Department -dc ContosoUniversityContext -outDir Controllers -f
    ```
    
    > 這裡的 `-f` 是 **Force** 的意思，如果你的專案已經存在即將產生的檔案，會直接覆蓋現有檔案。
    
6.  示範如何快速建立 `DepartmentsController` 控制器（不產生任何 Views 頁面與不使用任何 Layout 版面）
    
    請執行以下命令：
    
    ```bash
    dotnet aspnet-codegenerator controller -name DepartmentsController -async -nv -m Department -dc ContosoUniversityContext -outDir Controllers -f
    ```
    
    > 這裡的 `-nv` 是 **No View** 的意思，他會產生 MVC 所需的 Controller 與 Actions 內容，但是不自動產生 Views 檔案。
    
7.  示範如何快速建立 ASP.NET Core Identity UI 相關檔案
    
    請先安裝 NuGet 套件：[Microsoft.AspNetCore.Identity.UI](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI)
    
    ```bash
    dotnet add package Microsoft.AspNetCore.Identity.UI
    dotnet aspnet-codegenerator identity -h
    ```
    
    執行以下命令：
    
    ```bash
    dotnet aspnet-codegenerator identity --useDefaultUI --dbContext ApiIdentityContext
    ```
    
    > [Scaffold Identity in ASP.NET Core projects | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-3.1&tabs=netcore-cli&WT.mc_id=DT-MVP-4015686)
    

### 啟用偵錯紀錄

如果你想得知更多 `dotnet aspnet-codegenerator` 執行過程中的追蹤資訊，可以先設定好 `codegen_trace` 環境變數，即可讓 `dotnet aspnet-codegenerator` 在執行的時候顯示更多訊息。

-   Windows Command Prompt
    
    ```batch
    SET codegen_trace=1
    ```
    
-   Windows PowerShell
    
    ```powershell
    $env:codegen_trace=1
    ```
    
-   Linux shell
    
    ```sh1
    export codegen_trace=1
    ```
    

### 相關連結

-   [dotnet aspnet-codegenerator 命令 | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-3.1&WT.mc_id=DT-MVP-4015686)
-   [NuGet Gallery | dotnet-aspnet-codegenerator 3.1.4](https://www.nuget.org/packages/dotnet-aspnet-codegenerator)
