[ASP.NET Core 6 MVC] 使用 Dapper

這篇是參考自 Code Maze : Using Dapper with ASP.NET Core Web API 的教學,練習後自己做了一個筆記。以下使用 Visual Studio 2022,建立專案為 ASP.NET Core Web 應用程式 (Model-View-Controller)使用 .NET 6.0 架構,方案名稱為 DemoDapper。以下會講得比較瑣碎詳細,因為以前開始學 .NET 4.8 MVC 時欠缺這樣 step by step 的懶人包,希望可以幫到一些初學者。(以下忽略 null 值判斷與 try catch 處理)


安裝 Dapper 套件

在「方案總管」視窗,對「相依性」按滑鼠右鍵,點選「管理 NuGet 套件」(或是「工具」,「NuGet 套件管理員」點選「管理方案的 NuGet 套件」)

於「瀏覽」標籤頁搜尋以下兩個,選擇適合的版本安裝,下圖是已安裝好的標籤頁:

  • Dapper 作者 Sam Safforn, Marc Gravell, Nick Craver
  • System.Data.SqlClient 作者 Microsoft

常常看教學還要安裝一個和 Configuration 相關的,但我安裝以上兩個套件即可。


取得連線字串和設定 appsettings.json

以我的本機 MS SQL 為例,機器「.」使用Windows 驗證登入,有一個資料庫叫「dbtest」:

回到 Visual Studio,點選「檢視」,「伺服器總管」,在伺服器總管視窗的「資料連接」上或空白處按滑鼠右鍵,點選「加入連接」:

「選擇資料來源」頁面,「資料來源」選擇 Microsoft SQL Server,「資料提供者」選擇 .NET Framework Data Provider for SQL Server,點選繼續:

「加入連接」頁面在「伺服器名稱」填入「.」驗證選 Windows 驗證,然後下方「選取或輸入資料庫名稱」就可以下拉找資料庫了,我選了前面說的 dbtest 資料庫。當然如果 SQL Server 在別台機器甚至要登入的,就和登入一樣選擇 SQL Server 驗證並填入帳號密碼:

左下可以點「測試連接」,確認無誤後點「確定」。

再回到伺服器總管,會出現該資料庫,點開可確認是該資料庫的資料表,在連接名稱上按滑鼠右鍵選擇「屬性」,或是點選連接後按 F4:

在屬性視窗,有「連接字串」的值,將它全選複製。

然後在方案總管視窗找到並開啟檔案 appsettings.json:

原本的內容:

加上 "ConnectionStrings" (VS 2022 很聰明會自動幫忙列出,可以用點選的)屬性,內容自己加上一個 key value 組,key 名可以自取,value 是剛剛複製的連接字串,下面我命名為 "DemoConnection":

如果對連線字串的格式熟練,也是可以自己寫這一串。好了後將 appsettings.json 存檔即可。


新增介面與類別檔案

接下來有三種 .cs 類別檔案要新增,這裡我偷懶全部放在 ~/Models 資料夾裡。第一種是因為資料庫中有 Products 和 Orders 兩張資料表:

所以要有 Product 和 Order 兩個類別,類別屬性和資料表的欄位對應,也就是 DTO(Data Transfer Object)的概念,準備要在資料庫撈出資料後能接收並運用:

  namespace DemoDapper.Models
  {
    public class Product
    {
      public int ProductId { get; set; }
      public string Name { get; set; }
      public int Price { get; set; }
    }
    public class Order
    {
      public int OrderId { get; set; }
      public int ProductId { get; set; }
      public int Quantity { get; set; }
      public DateTime OrderDate { get; set; }      // 這個屬性名和 SQL 中的命名 Date 不同,之後撈資料要做 alias
      public string Name { get; set; }
      public int Price { get; set; }
    }
  }

第二種是 Dapper Context 物件,功用是用連線字串取得連線物件,然後連線物件有 Dapper 方法來操作資料庫:

  using System.Data;
  using System.Data.SqlClient;          // 取得 SqlConnection 連線物件需要引用的命名空間

  namespace DemoDapper.Models
  {
    public class DapperContext
    {
      private readonly IConfiguration _configuration;
      private readonly string _connectionString;

      public DapperContext(IConfiguration configuration)      // 建構時取得服務
      {
        _configuration = configuration;
        _connectionString = configuration.GetConnectionString("DemoConnection");
      }                                       // 在 appsettings.json 中設定的連線字串名稱

      public IDbConnection CreateConnection()
      {
        return new SqlConnection(_connectionString);
      }
    }
  }

第三種是處理 CRUD 邏輯的 service,在這裡我稱為 Factory,並且先做介面 IFactory 讓 ProductFactory 和 OrderFactory 來實作他:

  namespace DemoDapper.Models
  {
    public interface IFactory<T>       // 使用泛型,待實作時指定類別
    {
      public T GetOne(int id);         // 定義好三個方法,之後要實作出來
      public IEnumerable<T> GetAll();      
      public void Create(T entity);
    }
  }

而 ProductFactory 中要實作這些方法,為了運作要使用上面寫的 DapperContext 物件來取得連線物件,而 DapperContext 物件使用注入方式,在建構時取得:

  using Dapper;

  namespace DemoDapper.Models
  {
    public class ProductFactory : IFactory<Product>    // 實作 IFactory 介面且泛型指定形別為 Product
    {
      private readonly DapperContext _context;

      public ProductFactory(DapperContext context)     // 建構時取得 DapperContext 物件
      {
        _context = context;
      }
      public Product GetOne(int id)              // 依 ProductId 取得一個 Product 物件
      {
        string sql = $"SELECT * FROM Products WHERE ProductId = {id}";
        Product result = null;
        using(var connection = _context.CreateConnection())
        {                                       // 使用 DapperContext 物件回傳的連線物件
          result = connection.Query<Product>(sql).FirstOrDefault();
        }
        return result;
      }
      public IEnumerable<Product> GetAll()      // 取得所有 Product 物件
      {
        string sql = "SELECT * FROM Products";
        IEnumerable<Product> result = null;
        using(var con = _context.CreateConnection())
        {                                       // 使用 DapperContext 物件回傳的連線物件
          result = con.Query<Product>(sql);     // 將取出資料依欄位與屬性名稱對應,傳回 IEnuermable<Product> 物件
        }
        return result;
      }

      public void Create(Product entity)        // 新增一個 Product 資料並寫入資料庫
      {
        string sql = $"INSERT INTO Products (Name, Price) VALUES (@Name, @Price)";
        using (var con = _context.CreateConnection())
        {                                       // 使用 DapperContext 物件回傳的連線物件
          DynamicParameters paras = new DynamicParameters();    // 防 SQL Injection
          paras.Add("@Name", entity.Name);
          paras.Add("@Price", entity.Price);
          con.Execute(sql, paras);
        }
        return;
      }
    }
  }

OrderFactory 一樣要實做 GetOne、GetAll 和 Create 三個方法,但我這裡略過,增加了一個 Detail 方法,做為練習 Dapper join 語法

  using Dapper;

  namespace DemoDapper.Models
  {
    public class OrderFactory : IFactory<Order>    // 實作 IFactory 介面且泛型指定形別為 Order
    {
      private readonly DapperContext _context;
      public OrderFactory(DapperContext context)     // 建構時取得 DapperContext 物件
      {
        _context = context;
      }

      public IEnumerable<Order> Detail(int id)
      {         // 因為 SQL 中 Order 資料表存放的欄位名稱叫 Date,要另外 AS 成 Order 類別的屬性 OrderDate 才收的到
        string sql = "SELECT a.OrderId,a.Quantity,a.Date AS OrderDate,b.Name,b.Price FROM Orders AS a JOIN Products AS b on a.ProductId = b.ProductId";
        IEnumerable<Order> result = null;
        using(var con = _context.CreateConnection())
        {
          result = con.Query<Order>(sql);
        }
        return result;
      }
      public void Create(Order entity) { /* ... */ }
      public IEnumerable<Order> GetAll() { /* ... */ }
      public Order GetOne(int id) { /* ... */ }
    }
  }

要在 Program.cs 注入容器,使得 ProductFactory 和 OrderFactory 建構時能取得 DapperContext 物件:

  using DemoDapper.Models;

  var builder = WebApplication.CreateBuilder(args);
  // Add services to the container.
  
  builder.Services.AddSingleton<DapperContext>();       // 注入服務


Controller 與 View

邏輯類別完成後,再來是 Controller 和 View,新增一個 ProductController,裡面的運作需要 ProductFactory 物件,靠建構時取得:

  using DemoDapper.Models;
  using Microsoft.AspNetCore.Mvc;

  namespace DemoDapper.Controllers
  {
    public class ProductController : Controller
    {
      private readonly ProductFactory _factory;
      
      public ProductController(IFactory<Product> factory)   // 建構時取得 IFactory<Product> 物件
      {
        _factory = factory as ProductFactory;               // 指定並轉型給 _factory 供使用
      }

      public IActionResult List()            // 列出所有 Product 資料
      {
        IEnumerable<Product> result = _factory.GetAll();
        return View(result);
      }

      public IActionResult Show(int id)      // 依 id 取得一筆 Product 資料
      {
        Product result = _factory.GetOne(id);
        return View(result);
      }
      [HttpGet]
      public IActionResult Add()         // 新增一筆 Product 資料,
      {                                     // Get 先 return View() 得畫面
        return View();
      }
      [HttpPost]
      public IActionResult Add(Product newProduct)
      {                                     // Post 送出時新增到資料庫,並導向 List 的 View
        _factory.Create(newProduct);
        return RedirectToAction("List");
      }
    }
  }

新增 OrderController ,新增一個 action Detail,一樣地需要的 OrderFactory 在建構時取得:

  using DemoDapper.Models;
  using Microsoft.AspNetCore.Mvc;

  namespace DemoDapper.Controllers
  {
    public class OrderController : Controller
    {
      private readonly OrderFactory _factory;

      public OrderController(IFactory<Order> factory)   // 建構時取得 IFactory<Order> 物件
      {
        _factory = factory as OrderFactory;             // 指定並轉型給 _factory 供使用
      }

      public IActionResult Detail(int id)
      {
        IEnumerable<Order> result = _factory.Detail(id);
        return View(result);
      }
    }
  }

要在 Program.cs 注入容器使 Controller 建構時能取得實作了 IFactory<Product> 的 ProductFactory:

  using DemoDapper.Models;

  var builder = WebApplication.CreateBuilder(args);
  // Add services to the container.
  
  builder.Services.AddScoped<IFactory<Product>, ProductFactory>();
  builder.Services.AddScoped<IFactory<Order>, OrderFactory>();

剩下的 View 的程式碼就略過了,有需要的再上 GitHub 看,基本上就是把傳入的 model 列舉出來顯示,下面貼一些截圖:

~/Product/Add

~/Product/List

~/Order/Detail

留言