【.NET Core】使用EntityFramework並學習延伸概念

距離上次寫網誌的時間有點久,2022年想做個新專案,最近正在學習前端框架的技術,但還是要把.Net Core相關的網誌寫完,今天來講講常用的ORM吧。

ORM的基礎知識


什麼是ORM?
ORM,全名為Object Relational Mapping,中文可翻譯為物件關聯對應。後端工程師工作時會花很多時間在撰寫API,也會很常對資料庫進行一系列的操作,透過ORM可以讓我們在寫程式時,以更簡便的語法去對資料庫進行操作。不同程式語言各自發展出對應的ORM,在C#中常見的資料庫操作工作或套件有ADO.NETDapperEntityFramework。ADO.NET可透過下SQL語句的方式操作資料庫(參1),優點是如果對SQL語法的效能很熟悉,直接下SQL語法會是比較直觀的操作,但因為要防範SQL Injection,使用ADO,NET會大量的使用Parameter。而EntityFrame可以想像成被包裝成更方便的套件,同時使用物件導向,將物件對應到Model,但如果較複雜的使用需求,可能還是要搭配SQL語法一起使用。如果還是沒有感覺,就跟著一起練習吧,接下來阿猩會示範EntityFrameworkSQL Servcer的組合來進行說明。

動手做一做


建立新的.NET Core MVC專案

dotnet new mvc -o EF_Test

安裝EntityFramework

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

建立Model
剛剛上述提到EntityFramework除了可以操作資料庫以外,更重要的是會將資料以Model的形式來做處理,故我們需要先建立一個Model來代表資料庫的Table Schema。取得TableModel的方式阿猩試過3種

1. 自己手動建立Class
2. 使用.Net Framework建立項目中的資料,並透過DB First取得
3. 使用scaffold指令來完成

建立後的DB Schema與Model會長得像是圖1

圖1 資料庫資料與Table Schema Model

 

建立DbContext
接著是很關鍵的步驟,就是要建立一個Context的類別,這裡通常會包含是Table的名稱或是某一個共同工作會使用到的類別定義。物件名稱通常會以Context作為結尾(圖2-區塊1),並繼承DbContext,這樣才能使用EntityFramework裡的功能。而初始化所塞進來的DbContextOptions類別,可以讓我們後續使用DI的方式設定一些參數,例如連線字串(圖2-區塊2)。

圖片2-區塊3中的ModelBuilder,則可以讓我們做一些其他的設定,例如當Table有多個key值時,我們可以透過HasKey來進行設定

而最後的圖2-區塊4則是宣告資料庫中欄位的區塊,利用DbSet並指定Table Schema的型別,之後就可以使用這個property跟資料庫溝通並映射,例如這裡的Animals類別就是圖1所建立類別。

圖2 DbContext說明

 

建立連線字串
上面的步驟僅建立好DbContext的相關設定,但還缺少連線字串,如果沒有設定連線字是無法連到DB的。在建立DbContext的物件中,可以直接在區塊2內指定連線字串,但DbContext會隨著專案越來越多個,如果每個都寫死在裡面,某一天要改的時候就會很麻煩,而且還會有生命週期的問題。而.Net Core大量了使用DI的概念,透過註冊服務,可將連線字串統一寫在AppSettings中,而且還可以控制服務的生命週期,到底什麼是生命週期,後面章節會提到,這裡我們先照著步驟設定資料庫連線字串。

阿猩在使用MVC專案作為範例,不確定是因為.NET Core MVC專案本身就如此,還是因為阿猩使用了最新的.NET 6.0,原本在.NET 5.0的API專案中會有Startup可進行服務註冊,但這個測試專案居然沒有,好在研究一下發現概念差不多。
先在Program.cs中注入服務(圖3),這段功能就是要針對前面步驟建立的AnimalDataContext,設定資料庫連線字串,而連線字串會使用GetConnectionString去取得,這麼一來就可以隨時抽換資料庫連線字串啦,甚至可以一個DbContext使用2個連線字串呢!而連線字串的本尊則是放在appsettings.json中(圖4),一般工作時,通常也會將共用的參數放在appsettings.json中統一管理。

圖3 注入服務

 

圖4 設定連線字串

 

與資料庫溝通並取得資料
上述步驟已經將所有步驟準備好了,接著就是來測試設定是否都正確。首先要在HomeController.cs找到Index這個Action,這也是建立.NET MVC專案時,預設就會存在的Action,而且也有對應的View,所以阿猩就直接修改這個Action來做示範,如果是Web API的專案,可以透過PostMan或自行加入Swagger等方式來確認是否能正常取到資料庫的資料。

 

先將Index Action的程式碼改為下列

public IActionResult Index()
{
    List<Animals> Result = new List<Animals>();
    var Query = _AnimalDataContext.Animals.ToList();
    
    foreach(var item in Query)
    {
        Animals Data = new Animals();
        Data.Name = item.Name;
        Data.Color = item.Color;
        Data.Count = item.Count;
        Result.Add(Data);
    }
    
    return View(Result);
}

這裡的程式碼是表示,要使用AnimalDataContext,並指定Table(此範例為Animals),取得資料庫資料後儲存於Query變數,這裡阿猩另外使用一個新的List<Animals>去接資料,因為實際工作使用時,前端不一定需要所有的資料,更多時候會需要加入業務邏輯、型別轉換等額外處理,這裡只是為了做個概念性示範而故意這樣寫。

 

最後要補上DI的最後一塊拼圖,雖然我們在Programe中已經註冊服務了,但程式本身是不可能知道我們何時要使用資料庫的,所以我們需要在Controller中補上下列的程式碼,logger是本來預設就存在的,我們僅需要補上AnimalDataContext的宣告即可。

private readonly ILogger<HomeController> _logger;
private readonly AnimalDataContext _AnimalDataContext;
    

public HomeController(ILogger<HomeController> logger, AnimalDataContext AnimalDataContext)
{
    _logger = logger;
    _AnimalDataContext = AnimalDataContext;
}

 


修改Index.cshtml的內容,這裡的程式碼,目的就是將Controller帶過來的Result取出來並顯示在網頁畫面上,儲存後執行dotnet run看到結果就大功告成囉(圖5) 

@model List<EF_Test.Models.EF_Model.DBSetModel.Animals>
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    @foreach (var item in Model)
    {
        <div>
            <span>@item.Name</span>
            <span>@item.Color</span>
            <span>@item.Count</span>
        </div>
    }


</div>
圖5網頁成功畫面

其他延伸概念


三層式架構概要
前面步驟是為了快速測試而簡化很多流程的寫法,但實際在業界使用時,如果將所有程式碼都寫在Controller將會是一件很可怕的事情。一般後端要處理的業務大致有幾個要點。

1. 跟Client端相關:如果是MVC會碰到ViewBag等資料傳遞的方法,如果前後端分離,則會需要組成JSON字串。
2. 業務邏輯相關:因應不同的專案、客戶需求、API功能等,會有很多自訂義的業務邏輯。
3. 資料庫相關:例如對DB進行CRUD、將Entity Model轉換成View需要的Model等等

時間一長,隨著專案變大將會變得難以維護,加上分工的情況,就算有將功能以Class或Method的方法封裝,還是會碰到一些問題,例如命名、重複程式碼過多等,因此,最好的方法就是將各自的職責拆開。

簡單練習分層
Controller用來控制流程,業務邏輯跟對資料庫操作應該要抽出來封裝,所以正常情況下,不應該讓Controller出現跟Db細節的指令,Controller應該只能使用封裝好的功能,專心處理與前端相關的流程作業。我們另外建立Interface及Service的資料夾,建立Interface與Service,並將原本Controller的程式碼搬遷至Service中。

using EF_Test.Models.EF_Model.DBSetModel;
namespace EF_Test.Interfaces.Home
{
   public interface IHomeService
   {
         List<Animals> GetAnimals ();
   }
}
using EF_Test.Interfaces.Home;
using EF_Test.Models.EF_Model;
using EF_Test.Models.EF_Model.DBSetModel;
namespace EF_Test.Services.Home
{
   public class HomeService : IHomeService
   {
       private readonly AnimalDataContext _AnimalDataContext;
       public HomeService(AnimalDataContext AnimalDataContext)
       {
           _AnimalDataContext = AnimalDataContext;
       }
       public List<Animals> GetAnimals()
       {
           List<Animals> Result = new List<Animals>();
           var Query = _AnimalDataContext.Animals.ToList();
           foreach (var item in Query)
           {
               Animals Data = new Animals();
               Data.Name = item.Name;
               Data.Color = item.Color;
               Data.Count = item.Count;
               Result.Add(Data);
           }
           return Result;
       }
   }
}

Controller中的程式碼則需要改為

public class HomeController : Controller
{
   private readonly ILogger<HomeController> _logger;
   private readonly IHomeService _IHomeService;
   
   public HomeController(ILogger<HomeController> logger, IHomeService IHomeService)
   {
       _logger = logger;
       _IHomeService = IHomeService;
   }
   public IActionResult Index()
   {
       List<Animals> Result = _IHomeService.GetAnimals();
       return View(Result);
   }
}

執行dotnet run後開啟網頁會發現錯誤(圖6),原因是因為我們在Controller呼叫了IHomeService,但在Programe卻沒有註冊服務,所以就會噴錯,只要在Program.cs加上下列程式碼,就大功告成啦
 

圖6 未註冊所產生的錯誤訊息
builder.Services.AddScoped<IHomeService,HomeService>();

其中AddScoped就是上述一直提到的生命週期,根據服務的性質,有些可以共用,有些則需要在每次Request都重新實體化,像我們的範例,每個使用者進入網頁後,就會透過HomeController向資料庫要資料,但索取完資料丟給前端顯示後,就不需要再使用了,故使用Scope,能確保連線不會被長時間霸占,想了解更多的可參考(參2)。


AutoMap
將EntityFramework取出的資料送給前端時,阿猩刻意建立了一個List<Animals>去承接資料後才丟給前端,當Table 欄位變多的時候,一個一個手動賦值很麻煩也容易塞錯變數名稱。C#可使用Reflection進行類別的轉換,但對於新手而言,反射可能會比較艱深,這裡阿猩要介紹一個方便的套件AutoMap,透過AutoMap可快速的進行兩個類別之前的轉換,也可透過Formember進行較複雜的對應或忽略某些屬性等等,有興趣自行研究吧。

今天就講到這裡啦,阿猩祝大家2022年虎哩大發財~