﻿# 深入理解 ASP.NET MVC 與 ASP.NET Core MVC 擴充驗證屬性的改變

我在 2013 年的 [ASP.NET MVC 開發心得分享 (24)：擴充部分類別的建構子](/post/2013/06/18/ASPNET-MVC-Developer-Note-Part-24-Extend-Constructor-in-Partial-Method) 文章中，是我第一次介紹 .NET Framework 年代的 ASP.NET MVC 如何替「實體資料模型」(Entity Data Model) 在有使用 EDMX 自動產生程式碼的情況下如何還能擴充現有的「實體模型類別」(Entity Model Class) 的「驗證屬性」(Validation Attributes)。但直到 ASP.NET Core 的 MVC 開始，作法有點改變了，這篇文章我來記錄一下這些變化的地方。

### 準備範例程式

我為這篇文章準備了一個 ASP.NET Core 6 的範例專案，所有程式碼都會從 `master` 分支的最新版開始改起，初始化專案的步驟如下：

1.  取回原始碼
    
    ```bash
    git clone https://github.com/doggy8088/EFCore6Demo.git
    cd EFCore6Demo
    dotnet build
    ```
    
    這份原始碼已經包含了一個 `ContosoUniversity.db` 資料庫檔案 (SQLite)！
    
    > 備註: 如果你有修改 Entity Framework Core 的實體模型，才需要用以下命令更新資料庫。
    > 
    > ```bash
    > dotnet ef migrations add MIGRATION_NAME
    > dotnet ef database update -v
    > ```
    
2.  開啟 VS Code
    
    ```bash
    code .
    ```
    
3.  啟動專案
    
    ```bash
    dotnet watch run
    ```
    
4.  呼叫 API
    
    你可以直接用瀏覽器打開 [https://localhost:7295/api/Departments/1](https://localhost:7295/api/Departments/1) 即可看到結果。
    
    也可以透過 `curl` 進行測試：
    
    ```bash
    curl -s "https://localhost:7295/api/Departments/1"
    ```
    
    > 如果你在 Windows PowerShell 下執行上述命令，可能會看到類似 `cmdlet Invoke-WebRequest at command pipeline position 1` 的訊息而無法執行。若有這種狀況，請先執行 `Remove-Item Alias:curl` 移除掉系統內建的 `curl` 命令別名，然後再重新執行一次相同命令即可。
    
    你會得到以下結果：
    
    ```json
    {
      "departmentId": 1,
      "name": "教育訓練部",
      "budget": 1000,
      "startDate": "2022-04-20T23:38:36.372125",
      "instructorId": null,
      "instructor": null,
      "courses": []
    }
    ```
    
    > 這裡你可以看到 `courses` 這個屬性回傳一個「空陣列」，這意味著 Entity Framework Core 並不會透過「導覽屬性」載入 `Courses` 的關聯資料。
    
5.  按下 `Ctrl-C` 關閉 `dotnet watch run` 執行
    

### 替現有的「實體模型類別」擴充「驗證屬性」

由於我們在本專案的 `Models` 資料夾下，所有的 **實體模型類別** (Entity Model Class) 與 `ContosoUniversityContext` 類別都是透過 `dotnet ef dbcontext scaffold` 或 [EF Core Power Tools](https://marketplace.visualstudio.com/items?itemName=ErikEJ.EFCorePowerTools) 這類工具自動產生的，這類自動產生的程式碼，理論上我們不應該去手動修改這些類別，以免日後資料庫更新時這些客製化後的程式碼被覆蓋掉，那就不妙了。

因此，我們如果真的要對這些「實體模型類別」(Entity Model Class) 特別加上「驗證屬性」(Validation Attributes) 的話，就需要用到 [buddy class](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/getting-started-with-mvc/getting-started-with-mvc-part7?WT.mc_id=DT-MVP-4015686#:~:text=points%20to%20a%20%22-,buddy%20class,-%22%20with%20some%20attributes) 的開發技巧，透過 partial class (部分類別) 的擴充，將驗證屬性定義到另一個不會被**程式碼產生器工具**重新產生模型類別而覆蓋的檔案中！

這裡我以 `Models/Department.cs` 檔案為例，原始碼如下：

```csharp
using System;
using System.Collections.Generic;

namespace EFCore6Demo.Models
{
    public partial class Department
    {
        public Department()
        {
            Courses = new HashSet<Course>();
        }

        public int DepartmentId { get; set; }
        public string? Name { get; set; }
        public decimal Budget { get; set; }
        public DateTime StartDate { get; set; }
        public int? InstructorId { get; set; }

        public virtual Person? Instructor { get; set; }
        public virtual ICollection<Course> Courses { get; set; }
    }
}
```

我要擴充 `[Required]` 驗證屬性到 `Name`、`Budget` 與 `StartDate` 屬性的話，就會建立另一個 `Models/Department.Partial.cs` 檔案 (檔名不同)，其內容如下：

```csharp
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

#nullable disable

namespace EFCore6Demo.Models
{
    [ModelMetadataType(typeof(DepartmentMetadata))]
    public partial class Department
    {
    }

    internal class DepartmentMetadata
    {
        public Int32 DepartmentId { get; set; }

        [Required]
        public String Name { get; set; }

        [Required]
        public Decimal Budget { get; set; }

        [Required]
        public DateTime StartDate { get; set; }

        public Nullable<Int32> InstructorId { get; set; }
    }
}
```

以上就是替現有的「實體模型類別」擴充「驗證屬性」的標準方法！

### 介紹 Duotify.EFCore.EntityPartialGenerator 工具

可以想見，替每一個**實體模型類別**擴充**驗證屬性**這種方法是一種苦工，要是我有 100 個實體資料模型呢？那我不就要重複的工作做的一百遍嗎？！

沒錯，我就是專門為了這個需求，設計了一款 [Duotify.EFCore.EntityPartialGenerator](https://www.nuget.org/packages/Duotify.EFCore.EntityPartialGenerator) 工具，他會全自動的將目前專案中使用到的 DbContext 類別，自動分析出所有**實體模型類別**，並且自動產生上述程式碼的骨架，方便我們日後隨時可以擴充**驗證屬性**！😍

以下就是這個專案的使用方式：

1.  安裝工具
    
    ```bash
    dotnet tool install -g Duotify.EFCore.EntityPartialGenerator
    ```
    
2.  查詢使用方式
    
    ```bash
    efp
    ```
    
    > `efp` is stands for **Entity Framework Partial class generator**.
    
    ```
    Usage: efp generate -c ContosoUniversityContext -v -o Models
    
    Commands:
      list:  Lists available DbContext types.
      generate:  Generate a set of "buddy class" for all entity types in a DbContext
    ```
    
3.  列出專案中所有 DbContext 資料內容類別
    
    ```bash
    efp list
    ```
    
    輸出的 `ContosoUniversityContext` 就是你目前專案用到的 DbContext 類別：
    
    ```
    G:\Projects\EFCore6Demo>efp list
    Build started...
    Build succeeded.
    ContosoUniversityContext
    ```
    
4.  自動產生實體模型類別的 "buddy class"
    
    如果專案只有一個 DbContext 類別，而且預設輸出目錄也在 Models 的話，你可以這樣執行命令：
    
    ```bash
    efp generate
    ```
    
    ![自動產生實體模型類別的 "buddy class"](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/9c8efc01bb799961e82c-225312044-303bfa6e-d26f-49f0-9b20-2655e8c4170c.png)
    
    否則你也可以寫比較完整的命令如下：
    
    ```bash
    efp generate -c ContosoUniversityContext -o Models
    ```
    
    > 執行前會先建置專案，如果專案無法成功建置就無法產生程式碼！🔥
    
    如果想在產生程式碼的同時也顯示產生的檔案名稱，可以加上 `-v` 參數：
    
    ```bash
    efp generate -c ContosoUniversityContext -o Models -v
    ```
    
    如果想重新產生程式碼，且要覆寫現有已存在的檔案，可以加上 `-f` 參數：
    
    ```bash
    efp generate -c ContosoUniversityContext -o Models -v -f
    ```
    

### 相關連結

-   開源專案 ▶ https://github.com/doggy8088/Duotify.EFCore.EntityPartialGenerator
-   NuGet 套件 ▶ https://www.nuget.org/packages/Duotify.EFCore.EntityPartialGenerator
