2013年5月10日 星期五

ASP.NET MVC 的 Model 使用 ADO.NET

前幾天在公司做 ASP.NET MVC 的教育訓練,因為公司的 .NET 程式設計師大多沒有在既有的專案去導入使用 ADO.NET Entity Framework,所以在講述 ASP.NET MVC 的 Model 時,如果還是以 ADO.NET Entity Framework 為主來說明 Model 這一個部分的話,應該會讓聽的人感到無所適從,因為沒有實際用在專案開發上,就會感到不熟悉,而在 ASP.NET MVC 的學習過程就會有所阻礙,然後去排斥,這就不是我所期待的結果。

ASP.NET MVC 的 Model 並不是只能使用 ADO.NET Entity Framework。

但是很多想學習 ASP.NET MVC 的朋友無論在書裡或是官方網站的教學課程裡,甚至是網路上的教學文章,包括我這個小部落格裡大部分有關 ASP.NET MVC 的文章中,Model 這部份都是採用 ADO.NET Entity Framework,以致於很多人就有個詭異的觀念「ASP.NET MVC 的 Model 就是一定要用 ADO.NET Entity Framework 」,其實 ASP.NET MVC 的 Model 並不等同於 ADO.NET Entity Framework,只是 EF 是微軟官方所主推的 ORM Solution,所以在官方的教學課程裡大部分就只會看到 Model 採用 EF,而 ASP.NET MVC 強調強型別的使用,在 Controller 與 View 裡就能夠感受到強型別的優點,所以大部分有關 ASP.NET MVC 的書籍與文章都比較少去講 Model 使用非 ORM Solution 的內容。

其實 ADO.NET Entity Framework 本身也是架構在 ADO.NET 基礎上,底層還是使用了 ADO.NET,只是說 EF 幫我們做了很多事情,讓我們在寫程式的時候可以使用 LINQ 語法來做資料的存取操作等處理,不需要在去考慮到怎麼下 TSQL 來存取資料以及取資料後要放 DataReader or DataTable or DataSet or else 等等,現在大部分的 ASP.NET 程式設計人員在存取資料還是會使用 DataSet, DataTable 等弱型別的資料集合物件,其實使用這些弱型別資料集合物件並不是很方便,程式裡有很多地方都要去處理資料的型別轉換,但這種資料處理還是以 Database 的 Table 概念來思考的方式,很難將同樣的觀念與作法應用在 ASP.NET MVC 的開發上,然後就時常會聽到很多 ASP.NET WebForm 的開發人員在抱怨 ASP.NET MVC 很難,或是開發 ASP.NET MVC 時會想盡辦法的把以前開發 WebForm 的習慣帶到 MVC 上。

前言講了這麼多無非是要跟大家說開發 ASP.NET MVC 時不要有太多的包袱,如果開發 ASP.NET MVC 不想用 ADO.NET Entity Framework 的話,也是可以使用傳統 ADO.NET 的,ASP.NET MVC 架構對於用來建置 Model 並沒有任何特殊限制。

 


開發環境

Visual Studio 2012, MS SQL Server 2008 R2,

ASP.NET MVC 4,

.NET Framework 4.5

範例資料庫:Northwind

 

這邊要注意的是,我這邊並不是把所有的東西都放在一個 ASP.NET MVC 的專案裡,而是會依據需求去建立不同的類別庫專案,以下是我的方案結構,每個 project 再放置到不同的方案資料夾裡,

image

  • Domain:用來放置我們所建立的物件類別
  • Repository.Interface:放置 Repository 的 Interface
  • Reposotory.Implement:放置繼承實作 Repository.Interface 的 Project
  • Web:網站專案
  •  

    建立物件類別

    在方案裡建立一個類別庫專案,然後建立一個 Employee.cs 檔案,在這個檔案裡建立 Employee 類別以及其屬性,

    image

    namespace Sample.Domain
    {
        public class Employee
        {
          public int EmployeeID { get; set; }
          public string LastName { get; set; }
          public string FirstName { get; set; }
          public string Title { get; set; }
          public string TitleOfCourtesy { get; set; }
          public DateTime BirthDate { get; set; }
          public DateTime HireDate { get; set; }
          public string Address { get; set; }
          public string City { get; set; }
          public string Region { get; set; }
          public string PostalCode { get; set; }
          public string Country { get; set; }
          public string HomePhone { get; set; }
          public string Extension { get; set; }
          public string Notes { get; set; }
          public int? ReportsTo { get; set; }
          public string PhotoPath { get; set; }
     
        }
    }

     

    Repository Interface

    Repository Pattern 資料倉儲模式,把對資料庫存取的方法放在一個類別裡,這個資料倉儲的職責是在服務指定類別的資料存取,而這邊先建立介面,先定義我們會需要哪些的方法。

    建立一個類別庫專案「Sample.Repository.Interface」,加入 Sample.Domain 專案的參考,接著建立一個 IEmployeeRepository.cs 的介面,

    image

    因為是範例介紹,所以這裡先定義兩個方法就好,分別是取得一筆資料以及取得所有資料,

    namespace Sample.Repository.Interface
    {
        public interface IEmployeeRepository
        {
            Employee GetOne(int id);
     
            IEnumerable<Employee> GetEmployees();
     
        }
    }

     

    Repository Implement

    建立了 Domain 的類別,也建立了 Repository 的介面之後,接下來就是實作介面所定義的方法,這邊我們就要來使用 ADO.NET 對資料庫進行 Data Access,與上面的建立方式一樣,同樣是建立類別庫專案,加入 Sample.Domain 與 Sample.Repository.Interface 的參考,在專案內建立 EmployeeRepository.cs,而這個 EmployeeRepository 要去繼承實作 IEmployeeRepository 的內容,

    image

    下面是 EmployeeRepository.cs 的內容,繼承了 IEmployeeRepository 並且實作介面定義的方法,

    namespace Sample.Repository.ADONET
    {
        public class EmployeeRepository : IEmployeeRepository
        {
            public Employee GetOne(int id)
            {
                throw new NotImplementedException();
            }
     
            public IEnumerable<Employee> GetEmployees()
            {
                throw new NotImplementedException();
            }
        }
    }

    下面是完整的實作內容,

    namespace Sample.Repository.ADONET
    {
        public class EmployeeRepository : IEmployeeRepository
        {
            private string _connectionString;
            public string ConnectionString
            {
                get { return _connectionString; }
                set { _connectionString = value; }
            }
     
            public EmployeeRepository(string connectionString)
            {
                if (!string.IsNullOrWhiteSpace(connectionString))
                {
                    this._connectionString = connectionString;
                }
            }
     
            /// <summary>
            /// Gets the one.
            /// </summary>
            /// <param name="id">The id.</param>
            /// <returns></returns>
            /// <exception cref="System.NotImplementedException"></exception>
            public Employee GetOne(int id)
            {
                string sqlStatement = "select * from Employees where EmployeeID = @EmployeeID";
     
                Employee item = new Employee();
     
                using (SqlConnection conn = new SqlConnection(this.ConnectionString))
                using (SqlCommand comm = new SqlCommand(sqlStatement, conn))
                {
                    comm.Parameters.Add(new SqlParameter("EmployeeID", id));
     
                    if (conn.State != ConnectionState.Open) conn.Open();
     
                    using (IDataReader reader = comm.ExecuteReader())
                    {
                        if (reader.Read())
                        {
                            for (int i = 0; i < reader.FieldCount; i++)
                            {
                                PropertyInfo property = item.GetType().GetProperty(reader.GetName(i));
     
                                if (property != null && !reader.GetValue(i).Equals(DBNull.Value))
                                {
                                    ReflectionHelper.SetValue(property.Name, reader.GetValue(i), item);
                                }
                            }
                        }
                    }
                }
                return item;
            }
     
            /// <summary>
            /// Gets the employees.
            /// </summary>
            /// <returns></returns>
            /// <exception cref="System.NotImplementedException"></exception>
            public IEnumerable<Employee> GetEmployees()
            {
                List<Employee> employees = new List<Employee>();
     
                string sqlStatement = "select * from Employees order by EmployeeID";
     
                using (SqlConnection conn = new SqlConnection(this.ConnectionString))
                using (SqlCommand command = new SqlCommand(sqlStatement, conn))
                {
                    command.CommandType = CommandType.Text;
                    command.CommandTimeout = 180;
     
                    if (conn.State != ConnectionState.Open) conn.Open();
     
                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            Employee item = new Employee();
     
                            for (int i = 0; i < reader.FieldCount; i++)
                            {
                                PropertyInfo property = item.GetType().GetProperty(reader.GetName(i));
     
                                if (property != null && !reader.GetValue(i).Equals(DBNull.Value))
                                {
                                    ReflectionHelper.SetValue(property.Name, reader.GetValue(i), item);
                                }
                            }
                            employees.Add(item);
                        }
                    }
                }
                
                return employees;
            }
        }
    }

    在 GetOne 與 GetEmployees 這兩個方法都是向資料庫執行 SQL Statement 後取得資料,取得資料後再去對應到我所定義的 Employee 類別的屬性,這邊並不是使用一個個欄位對應屬性的方式,因為這麼做太累了,這邊是使用反射的方式來處理對應的部份,程式中的 ReflectionHelper.SetValue() 方法是我自己所做的,內容就不公布,不過類似的反射對應實作可以參考「微軟 MVC - 小朱」的文章「[ASP.NET][MVC] ASP.NET MVC (3) : 加入資料檢視功能-Models - 小朱® 的技術隨手寫- 點部落」內的作法。

     

    Web 專案

    最後就是建立一個 ASP.NET MVC 網站專案,專案加入 Sample.Domain, Sample.Repository.Interface, Sample.Repository.ADONET,

    image

    在 ~/Controllers 目錄下建立一個 EmployeeController.cs,並且新增兩個 Action 方法,以下是實作的內容,

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using Sample.Repository.ADONET;
    using Sample.Repository.Interface;
     
    namespace Sample.Web.MVC.Controllers
    {
        public class EmployeeController : Controller
        {
            private IEmployeeRepository _repository;
     
            public EmployeeController()
            {
                this._repository = new EmployeeRepository(MvcApplication.ConnectionString);
            }
            
            public ActionResult Index()
            {
                var employees = this._repository.GetEmployees();
                return View(employees);
            }
     
            public ActionResult Details(int id)
            {
                var employee = this._repository.GetOne(id);
                return View(employee);
            }
     
        }
    }

    先建立 Index 對應的 View,勾選「建立強型別檢視」選擇「Employee (Sample.Domain)」,使用「List」Scaffold 樣板,

    SNAGHTML330e511

    再來建立 Details 所使用的 View,勾選「建立強型別檢視」選擇「Employee (Sample.Domain)」,使用「Details」Scaffold 樣板,
    SNAGHTML3361880

    就不用看這兩個建立的 View 檔案內容,直接執行來看結果,

    Index

    image

    Details

    image

    完成。

     

    補充:

    KKBruce 把文章看得好仔細呀,一下就看到我刻意隱藏的地方(想要偷懶還是被抓包),其實那個 ReflectionHelper.SetValue() 方法我是取用之前開發團隊的程式,因為有很多人編修過,每個人都有參一腳,所以不方便公開,但還是需要給個交待,好讓想要了解這箇中原委的人可以知道,我補上一個簡單的作法,雖然 method 名稱不同,但方法的實作內容是類似的。

    ReflectionHelper.SetPropertyValue()

    #region SetPropertyValue
    /// <summary>
    /// SetPropertyValue.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    /// <param name="val">The val.</param>
    /// <param name="instance">The instance.</param>
    public void SetPropertyValue(object instance, string propertyName, string val)
    {
        if (null == instance) return;
     
        Type type = instance.GetType();
        PropertyInfo info = GetPropertyInfo(type, propertyName);
     
        if (null == info) return;
     
        try
        {
            if (info.PropertyType.Equals(typeof(string)))
            {
                info.SetValue(instance, val, new object[0]);
            }
            else if (info.PropertyType.Equals(typeof(Boolean)))
            {
                bool value = false;
                value = val.ToLower().StartsWith("true");
                info.SetValue(instance, value, new object[0]);
            }
            else if (info.PropertyType.Equals(typeof(int)))
            {
                int value = String.IsNullOrEmpty(val) ? 0 : int.Parse(val);
                info.SetValue(instance, value, new object[0]);
            }
            else if (info.PropertyType.Equals(typeof(double)))
            {
                double value = 0.0d;
                if (!String.IsNullOrEmpty(val))
                {
                    value = Convert.ToDouble(val);
                }
                info.SetValue(instance, value, new object[0]);
            }
            else if (info.PropertyType.Equals(typeof(DateTime)))
            {
                DateTime value = String.IsNullOrEmpty(val)
                    ? DateTime.MinValue
                    : DateTime.Parse(val);
                info.SetValue(instance, value, new object[0]);
            }
        }
        catch
        {
            throw;
        }
    }
    #endregion

     

    相關系列文章

    ASP.NET MVC

    ASP.NET MVC 的 Model 使用 ADO.NET

    ASP.NET MVC 的 Model 使用 Enterprise Library 6 Data Access Application Block

    ASP.NET MVC - 使用 Simple Injector 讓 Model 三選一

    ASP.NET WebForm

    ASP.NET WebForm 使用分層的 Repository 類別庫專案

    ASP.NET WebForm 使用 Simple Injector 選擇不同的 Repository

    範例原始檔

    ASP.NET MVC 與 ASP.NET WebForm 使用 Simple Injector 切換選擇不同 Repository 原始碼下載

     


    這一次是做得比較細,分成了好幾個專案來做,這樣的作法雖然看起來很瑣碎,但提供了彈性,假如日後需要把資料存取的實作由原本的 ADO.NET 改換成 ADO.NET Entity Framework,只需要多建立一個 Sample.Repository.EF 類別庫專案,同樣繼承實作 Sample.Repository.Interface 的介面內容,最後 Web 專案內把原本使用 Sample.Repository.ADONET 換成 Sample.Repository.EF 就可以了,不需要去重寫或是修改原本 ADO.NET 存取資料的程式內容。

    我目前的工作還是會遇到開發 ASP.NET WebForm 的時候,如果無法使用 ADO.NET Entity Framework 而必須使用 ADO.NET 對資料庫存取時,會盡量避免在專案裡去使用 DataSet 或 DataTable,然後也不會在頁面上使用 DataSource Control,在資料存取處理上,我會依照需求去建立不同的物件,例如會員、訂單、產品等等各種不同的 Class,然後使用 ADO.NET 向資料庫取得資料後在 Mapping 到我所定義的物件屬性上;或許很多人會覺得這樣要事先建立 Class 然後取得資料後要做 Mapping 的動作很瑣碎而且也不快,其實有這樣想法的人只把這些前置作業與 Mapping 的程式編寫所花的時間給放大了好幾倍,而忽略了強型別在後續程式編寫以及系統維護時所帶來的好處。

    如果還是使用 DataSet, DataTable,那麼每次的存取都要花很多的時間做取出、型別轉換的動作,光是這些時間的耗費,我想絕對是比事先建立物件、取得資料後的 Mapping 的時間還要多上很多。

    前面的廢話真的很多,但是到了最後還是要強調一下,不是說 ASP.NET MVC 使用 ADO.NET 時就不能使用 DataSet, DataTable,而是要盡量避免去使用,能不用就不要用!

     

    延伸閱讀:

    [ASP.NET][MVC] ASP.NET MVC (3) : 加入資料檢視功能-Models - 小朱® 的技術隨手寫- 點部落

    KingKong Bruce記事: ASP.NET MVC裡Model使用ADO.NET來開發

     

    以上

    14 則留言:

    1. 好讚的文章。
      但沒看到 ReflectionHelper.SetValue() 的實作總覺得少了什麼殺傷力。 ^_^

      回覆刪除
      回覆
      1. 看得好仔細呀~
        因為那個同樣的作法在網路上有很多人都有很好的實作方法,我實在不敢野人獻曝。

        刪除
    2. 請問版主,我們又該如何設計系統架構(三層式),不論展示層是使用 asp.net 或是 asp.net mvc 皆可任意套用?

      回覆刪除
      回覆
      1. Hello,
        你可以參考這部落格裡的「分層架構」系列文章,雖然是以 ASP.NET MVC 為使用情境,但是概念上在 ASP.NET WebForm 應用上是一樣的,又或者是你可以期待下一篇文章,將會講如何以本篇所延伸出來的架構使用在 ASP.NET WebForm 裡。

        刪除
    3. 您好
      請問一下
      您後來補充的SetPropertyValue
      裡面有呼叫
      PropertyInfo info = GetPropertyInfo(type, propertyName);
      的GetPropertyInfo 可以提供嗎?

      感謝

      回覆刪除
      回覆
      1. 請參閱以下這篇文章
        「ASP.NET MVC 與 ASP.NET WebForm 使用 Simple Injector 切換選擇不同 Repository 原始碼下載」
        http://kevintsengtw.blogspot.tw/2013/05/aspnet-mvc-aspnet-webform-simple.html

        整個系列的原始碼都完整提供.

        刪除
      2. 下載研究中
        好完整的範例
        感謝

        刪除
    4. 從db取值mapping到物件的方法,懶得看細節的人,可以選擇dapper for .net這很名的mapping延伸物件,它幾乎比網上所有人寫得mapping helper更好,主要是有人在更改維護。

      回覆刪除
      回覆
      1. Hello, 你好
        你說得沒錯,Dapper 是個好物,也是值得介紹並推廣給大家來使用,
        只是...... 一般臺灣的公司與企業還有大部分的開發人員對於這樣的第三方套件,
        尤其是對資料庫操作存取的套件,接納度很低,
        不說別的,就連已經出來六七年的 EF,還是有一堆的 ASP.NET 開發人員從來沒有接觸過,
        這是我多次教課後的心得。

        刪除
    5. 哈囉 你好:
      請教一下!我在 Sample.Repository.ADONET 的EmployeeRepository.cs檔
      有'ReflectionHelper' 由於其保護層級之故,所以無法存取。 之問題?
      查找很久找不到原因,我有哪兒沒做好設定嗎?

      回覆刪除
      回覆
      1. 我不太懂你的問題耶
        另外你是直接下載 Github 的專案所發生的問題嗎

        刪除
      2. 你好:
        謝謝你回覆我!不好意思 我的問題敘述沒有很好!!
        直接下載Github 的專案 沒有問題!我另外自己在開設的專案 直接練習一次 因為是自己開設的練習有問題 想不明白為什麼?
        留言的問題我無法附上圖檔 做說明 請問可以跟你留一下EMAIL或其他聯絡方式嗎?

        刪除
      3. 留言時,有沒有看到左邊有顯示「詢問與建議」...
        其實你所出現的狀況,多看個幾次就可以知道,或者找個 code compare 的工具將你的程式與我的程式作個比對就可以知道,
        如果對於程式設計初學者來說,很容易忽略或沒有注意到類別或方法的修飾詞,
        如果已經是有幾年經驗的程式開發人員的話,就應該去面壁思過了
        拿「其保護層級之故,所以無法存取」去 google 搜尋,其實就可以知道答案

        刪除
    6. 網誌管理員已經移除這則留言。

      回覆刪除

    提醒

    千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。