The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何使用 .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 專案

    dotnet new webapi -n api1 && cd api1
    
  2. 安裝 dotnet-ignore 全域工具 (.NET CLI Global Tool)

    dotnet tool update -g dotnet-ignore
    
  3. 加入 .gitignore 檔案

    dotnet ignore get -n visualstudio
    
  4. 將專案加入 Git 版控

    git init && git add . && git commit -m "Initial commit"
    
  5. 在資料庫中建立 ContosoUniversity 範例資料庫

    建議使用 SQL Server Express LocalDB 即可:(localdb)\MSSQLLocalDB

  6. 安裝 dotnet-ef 全域工具 (.NET CLI Global Tool)

    dotnet tool update -g dotnet-ef
    
  7. 安裝 Entity Framework Core 相關套件

    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
    

    建立新版本

    git add . && git commit -m "Add EFCore NuGet packages"
    
  8. 透過 dotnet-ef 快速建立 EFCore 模型類別與資料內容類別

    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.'

    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 原始碼中,建議你將這段程式碼移除。

    建立新版本

    git add . && git commit -m "Create EFCore models and dbcontext classes using dotnet-ef"
    
  9. 調整 ASP․NET Core 的 Startup 類別並對 ContosoUniversityContext 設定 DI

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ContosoUniversityContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddControllers();
    }
    
  10. 調整 ASP․NET Core 的 appsettings.json 加入 DefaultConnection 連接字串

    {
      "ConnectionStrings": {
          "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Initial Catalog=ContosoUniversity;Integrated Security=True"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*"
    }
    

    建立新版本

    git add . && git commit -m "Add DI for ContosoUniversityContext and configure ConnectionStrings"
    
  11. 建置專案,確認可以正常編譯!

    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,其實背後也是幫你安裝相同的套件!

    請用以下命令安裝:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    

    建立新版本

    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 頁面)

    請先嘗試執行以下命令:

    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

    這點真的非常地雷,各位務必要特別注意類別名稱大小寫必須正確輸入!

    正確:ContosoUniversityContext

    錯誤:ContosouniversityContext

    正確的命令如下:

    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 版面)

    請執行以下命令:

    dotnet aspnet-codegenerator controller -name DepartmentsController -async -scripts -udl -m Department -dc ContosoUniversityContext -outDir Controllers
    

    這裡的 -udlUse Default Layout 的意思,如果你的專案有現成的 Layout 可用,請務必加上這個參數選項。

  5. 示範如何快速建立 DepartmentsController 控制器(包含 Views 頁面與不使用任何 Layout 版面)

    請執行以下命令:

    dotnet aspnet-codegenerator controller -name DepartmentsController -async -scripts -m Department -dc ContosoUniversityContext -outDir Controllers -f
    

    這裡的 -fForce 的意思,如果你的專案已經存在即將產生的檔案,會直接覆蓋現有檔案。

  6. 示範如何快速建立 DepartmentsController 控制器(不產生任何 Views 頁面與不使用任何 Layout 版面)

    請執行以下命令:

    dotnet aspnet-codegenerator controller -name DepartmentsController -async -nv -m Department -dc ContosoUniversityContext -outDir Controllers -f
    

    這裡的 -nvNo View 的意思,他會產生 MVC 所需的 Controller 與 Actions 內容,但是不自動產生 Views 檔案。

  7. 示範如何快速建立 ASP.NET Core Identity UI 相關檔案

    請先安裝 NuGet 套件:Microsoft.AspNetCore.Identity.UI

    dotnet add package Microsoft.AspNetCore.Identity.UI
    dotnet aspnet-codegenerator identity -h
    

    執行以下命令:

    dotnet aspnet-codegenerator identity --useDefaultUI --dbContext ApiIdentityContext
    

    Scaffold Identity in ASP.NET Core projects | Microsoft Docs

啟用偵錯紀錄

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

  • Windows Command Prompt

    SET codegen_trace=1
    
  • Windows PowerShell

    $env:codegen_trace=1
    
  • Linux shell

    export codegen_trace=1
    

相關連結

留言評論