2013年5月12日 星期日

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

前面兩篇有關 ASP.NET MVC Model 的文章向大家說明使用 ADO.NET 來處理 Model 的資料存取操作,還有如何使用 Enterprise Library 6 Data Access Application Block 來輔助並強化傳統 ADO.NET 資料存取的操作,

ASP.NET MVC 的 Model 使用 ADO.NET

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

這一篇的內容在文章標題就說得很清楚了,在這篇文章裡要向各位說明如何使用 Simple Injector 這個 DI/IoC Container 來讓 ASP.NET MVC 網站可以抽換不同的資料存取方式,這篇文章會再加上大家比較常見的 ADO.NET Entity Framework,藉由使用 Simple Injector 來達到網站選用三種不同資料存取方式的需求。

 


加入使用 ADO.NET Entity Framework 的 Repository

跟之前加入 ADO.NET Repositrory 與 EntLib DAAB Repository 的方式一樣,新增加一個類別庫專案,名稱為「Sample.Repository.EF」,建立完成後加入 Sample.Domain 與 Sample.Repository.Interface 的參考,

image

建立 EmployeeRepository.cs 然後繼承 Sample.Repository.Interface 的 IEmployeeRepository 然後實作介面所定義的方法,

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

先不進行方法的內容詳細實作,先來加入 ADO.NET 實體資料模型,

image

在 Sample.Web.MVC 網站裡所使用的 Entity 類別是使用 Smaple.Domain 裡所定義的類別,所以從 EF 索取的的物件就必須 Mapping 到 Sample.Domain 類別,這邊我就在 Smaple.Repository.EF 加入 AutoMapper 來處理物件對應的工作,有關 AutoMapper 的說明可以參考之前的文章「使用 AutoMapper 處理類別之間的對映轉換

image

EmployeeRepository 的方法內容詳細實作:

namespace Sample.Repository.EF
{
    public class EmployeeRepository : IEmployeeRepository
    {
        private NorthwindEntities db = new NorthwindEntities();
 
        /// <summary>
        /// Gets the one.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <returns></returns>
        public Sample.Domain.Employee GetOne(int id)
        {
            var employee = this.db.Employees.FirstOrDefault(x => x.EmployeeID == id);
 
            if (employee != null)
            {
                Mapper.CreateMap<Employee, Sample.Domain.Employee>();
                Domain.Employee instance = Mapper.Map<Sample.Domain.Employee>(employee);
                return instance;
            }
            return null;
        }
 
        /// <summary>
        /// Gets the employees.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Sample.Domain.Employee> GetEmployees()
        {
            var employees = this.db.Employees.OrderBy(x => x.EmployeeID);
 
            Mapper.CreateMap<Employee, Sample.Domain.Employee>();
            List<Sample.Domain.Employee> result = Mapper.Map<List<Sample.Domain.Employee>>(employees);
 
            return result;
        }
    }
}

 

Web 專案加入並使用 Sample.Repository.EF

加入 Smaple.Respository.EF 的專案參考

image

記得把 Sample.Repository.EF 裡 App.Config 的 ConnectionString 複製並加入到 Web 的 Web.Config 中,

image

修改 EmployeeController 的內容,這邊修改的有:使用 Sample.Repository.EF 命名空間的使用、EmployeeController 建構式裡建立 EmployeeRepository 實例的方式,

using System.Web.Mvc;
using Sample.Repository.EF;
using Sample.Repository.Interface;
 
namespace Sample.Web.MVC.Controllers
{
    public class EmployeeController : Controller
    {
        private IEmployeeRepository _repository;
 
        public EmployeeController()
        {
            this._repository = new EmployeeRepository();
        }
        
        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);
        }
    }
}

執行網站(這邊把 List 頁面裡的表格欄位拿掉幾個,然後 Index, Details 頁面重新套用預設 Layout),

image

image

 


現在我們的 Solution 底下有了三種不同資料存取方式的 Repository,

image

現階段如果 Web 要更換 Repository 的方式是必須要更換 Controller 中 Repository Namesapce 以及修改 Controller 建構式裡 Repository 的實例建立方式,我所做的操作範例只有很簡單的一個 EmployeeController 而已,所以手動更換看起來還算是輕鬆簡單,但如果 Web 專案裡有為數可觀的 Controller 時,我們還需要這樣一個一個 Controller 去做更換嗎?(不要跟我可以使用尋找並取代字串的功能 ……),程式設計師應該要聰明一點,之前我有介紹了 Unity.MVC 以及 Unity bootstrapper for ASP.NET MVC,

ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC

ASP.NET MVC 4 使用 Unity bootstrapper for ASP.NET MVC

我們可以使用 DI/IoC Container 來解決這個問題,而這次不再繼續沿用 Unity,雸是要向各位介紹另一套使用、設定上也相當容易的 DI/IoC Container - 「Simple Injector」。

 

Simple Injector

https://simpleinjector.codeplex.com/

Simple Injector 有針對 ASP.NET MVC 發佈了「Simple Injector ASP.NET MVC Integration」,我們可以在 Visual Studio  裡透過 Nuget 安裝到 Web 專案中。

 

Web 專案加入 Simple Injector

開啟 Nuget 並查詢「Simple injector」,查詢結果有相當多有關 Simple Injector 的套件,我們只需要安裝「Simple Injector MVC Interration Quick Start」就好,會把相依的其餘套件也一起安裝。

image

安裝完套件之後會在 Web 專案的 App_Start 目錄裡新增一個「SimpleInjectorInitializer.cs」檔案,

image

還沒有 「SimpleInjectorInitializer.cs」檔案進行修改前將方案做重新建置,會有一個錯誤,

image

錯誤的地方就在 SimpleInjectorInitializer 裡面的 InitializeContainer() 方法裡,

image

而 InitializeContainer() 就是我們要設定類別註冊的地方,但是這邊我們會遇到一個小小的問題,那就是註冊到 container 的類別,要對應 IEmployeeRepository 該用那一個類別呢?

image

我們也可以直接在 SimpleInjectorInitializer.cs 裡去增加 namespace 的使用,

image

增加 namespace 的 using 也是一種方法,不過我這邊想要用另一種方法,在 Web.Config 裡增加一個 AppSetting 「RepositoryType」,經由修改 RepositoryType 的值來決定要使用哪一個 Repository,

image

在 Global.asax 裡增加一個屬性,用來讀取 Web.Config 裡 Appsetting RepositoryType 的值,

image

在 ReflectionHelper 裡增加必要的方法,用來讀取 Assembly 裡指定的類別,

namespace Sample.Domain
{
    public class ReflectionHelper
    {
        /// <summary>
        /// Gets the type.
        /// </summary>
        /// <param name="pathOrAssemblyName">Name of the path or assembly.</param>
        /// <param name="classFullName">Full name of the class.</param>
        /// <returns></returns>
        public static Type GetType(string pathOrAssemblyName, string classFullName)
        {
            try
            {
                if (!pathOrAssemblyName.Contains(Path.DirectorySeparatorChar.ToString()))
                {
                    string assemblyName = AbstractAssemblyName(pathOrAssemblyName);
                    if (!classFullName.Contains(assemblyName))
                    {
                        classFullName = String.Concat(assemblyName, ".", classFullName);
                    }
                    Assembly assembly = Assembly.Load(assemblyName);
                    return assembly.GetType(classFullName);
                }
 
                Assembly asm = Assembly.LoadFrom(pathOrAssemblyName);
                if (null == asm) return null;
 
                Type type = asm.GetType(classFullName);
 
                if (null == type)
                {
                    foreach (Type one in asm.GetTypes())
                    {
                        if (one.Name == classFullName)
                        {
                            type = one;
                            break;
                        }
                    }
                }
                return type;
            }
            catch (Exception)
            {
                return null;
            }
        }
 
        /// <summary>
        /// Abstracts the name of the assembly.
        /// </summary>
        /// <param name="assemblyName">Name of the assembly.</param>
        /// <returns></returns>
        private static string AbstractAssemblyName(string assemblyName)
        {
            string prefix = ".\\";
            string suffix = ".dll";
 
            if (assemblyName.StartsWith(prefix))
            {
                assemblyName = assemblyName.Substring(prefix.Length);
            }
            if (assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
            {
                assemblyName = assemblyName.Substring(0, assemblyName.Length - suffix.Length);
            }
            return assemblyName;
        }
    }
}

接著修改 SimpleInjectorInitializer.cs 裡面的 InitializeContainer() 方法內容,

image

private static void InitializeContainer(Container container)
{
    // Register Type for Repository
 
    string repositoryType = MvcApplication.RepositoryType;
 
    container.Register(typeof(IEmployeeRepository),
        ReflectionHelper.GetType(repositoryType, 
            string.Concat(repositoryType, ".EmployeeRepository")));
}

最後就是要去調整 EmployeeController 的內容,我們不需要去增加 Repository 的 namespace,另外在 Controller 的建構式也不用直接去建立一個 Repository 的實例,這部份就交給 Simple Injector 來處理,

image

完成上面的調整步驟之後,最後我們執行網站來試試看,

image

image

我在 _layout.cshtml 增加目前使用的 Repository Type 的字串顯示,這樣就可以讓大家了解目前是使用哪一種 Repository Type。

接著修改 Web.Config 的 AppSetting 內容,改用 Sample.Repository.ADONET 來測試看看,

image

執行後卻發生錯誤,

image

這是因為我們當初建立 Sample.Repository.ADONET 的 EmployeeRepository 時,裡面的建構式是要傳入 connectionString 的值,

image

同樣的在 Sample.Repository.EntLibDAAB 的 EmployeeRepository 也是一樣,但不是給 connectionString 而是給 connectionStringName,

image

這邊我做的修改方式就是把上面兩個建構式有需要傳值進去的做個修改,不由外面傳值進去,讓它們去讀取 Web.Config 的值,所以之後要做資料庫連結字串內容的變動,就去修改 Web.Config 的內容就可以,

 

Sample.Repository.ADONET - EmployeeRepository 的修改

這邊也新增一個 BaseRepository 的抽象類別:

using System.Configuration;
 
namespace Sample.Repository.ADONET
{
    public abstract class BaseRepository
    {
        private string _connectionString;
        public string ConnectionString
        {
            get { return _connectionString; }
            set { _connectionString = value; }
        }
 
        public BaseRepository()
        {
            this.ConnectionString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        }
    }
}

修改 EmployeeRepository 內容:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using Sample.Domain;
using Sample.Repository.Interface;
 
namespace Sample.Repository.ADONET
{
    public class EmployeeRepository : BaseRepository, IEmployeeRepository
    {
        /// <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;
        }
    }
}

 

Sample.Repository.EntLibDAAB - EmployeeRepository 的修改

這邊我們有建立 BaseRepository 的抽象類別,所以先修改 BaseRepository 的內容:

using Microsoft.Practices.EnterpriseLibrary.Data;
 
namespace Sample.Repository.EntLibDAAB
{
    public abstract class BaseRepository
    {
        private DatabaseProviderFactory factory = new DatabaseProviderFactory();
 
        private string connectionStringName = "Northwind";
        
        private Database db;
        protected Database Db
        {
            get
            {
                if (this.db == null)
                {
                    this.db = this.factory.Create(this.connectionStringName);
                }
                return this.db;
            }
        }
 
        public BaseRepository()
        {
        }
 
    }
}

再來就是修改 EmployeeRepository 的內容,這邊要做的只有把建構式給拿掉,

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using Microsoft.Practices.EnterpriseLibrary.Data;
using Sample.Domain;
using Sample.Repository.Interface;
 
namespace Sample.Repository.EntLibDAAB
{
    public class EmployeeRepository : BaseRepository, IEmployeeRepository
    {
        /// <summary>
        /// Gets the one.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <returns></returns>
        public Employee GetOne(int id)
        {
            string sqlStatement = "select * from Employees where EmployeeID = @EmployeeID";
 
            DataAccessor<Employee> accessor =
                this.Db.CreateSqlStringAccessor<Employee>(
                    sqlStatement,
                    new EmployeeIDParameterMapper(),
                    new EmployeeMapper());
 
            return accessor.Execute(new object[] { id }).FirstOrDefault();
        }
 
        /// <summary>
        /// Gets the employees.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        public IEnumerable<Employee> GetEmployees()
        {
            string sqlStatement = "select * from Employees order by EmployeeID";
 
            DataAccessor<Employee> accessor =
                this.Db.CreateSqlStringAccessor<Employee>(sqlStatement, new EmployeeMapper());
 
            return accessor.Execute();
        }
 
    }
 
    public class EmployeeIDParameterMapper : IParameterMapper
    {
        public void AssignParameters(DbCommand command, object[] parameterValues)
        {
            var param = command.CreateParameter();
            param.ParameterName = "EmployeeID";
            param.Value = parameterValues[0];
            command.Parameters.Add(param);
        }
    }
 
    public class EmployeeMapper : IRowMapper<Employee>
    {
        public Employee MapRow(IDataRecord reader)
        {
            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);
                }
            }
            return item;
        }
    }
}

 

總算大功告成!不過最後還是另外裝了 MiniProfiler,這是用來觀測三種資料存取方式的執行效率,這部落格裡有很多 MiniProfiler 可以參考「mrkt:MiniProfiler 相關文章」,以下是各種資料存取方式的執行結果 。

 

Sample.Repository.ADONET

第一次執行

image

image

第二次執行

image

 

Sample.Repository.EntLibDAAB

第一次執行

image

image

第二次執行

image

 

Sample.Repository.EF

第一次執行

image

image

第二次執行

image

 

相關系列文章

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 原始碼下載

 


延續了前面兩篇文章的內容與架構,這一次增加了 ASP.NET MVC Model 應用中最常使用的 ADO.NET Entity Framework,在 Sample.Repository.EF 專案裡有使用了之前介紹過的 AutoMapper,讓 EF 的 Entity 類別透過 AutuMapper 對應到我們自行定義的 Domain 物件類別,最後在 Sample.Web.MVC 網站專案裡加入了 Simple Injector 這個 DI/IoC Container,搭配反射與 Simple Injector 的應用,讓我們可以只要修改 Web.Config 的 AppSetting 內容的情況下,讓 Web 專案切換不同的資料存取方式。

 

延伸閱讀:

Simple Injector Documentation

IoC Container Benchmark - Performance comparison - www.palmmedia.de

(給大家看 benchmark 不是要讓大家去用速度做選擇 IoC Container 的依據,而是讓大家知道有這麼多的選擇,並且依據自己專案的需求以及本身開發技術的熟悉度來選擇 IoC Container,也多了解各個 IoC Container 支援了哪些功能)

ASP.NET MVC 的 Model 使用 ADO.NET

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

使用 AutoMapper 處理類別之間的對映轉換

ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC

ASP.NET MVC 4 使用 Unity bootstrapper for ASP.NET MVC

mrkt:MiniProfiler 相關文章

 

以上

沒有留言:

張貼留言

提醒

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