2017年12月11日 星期一

如何不寫註解也看得懂程式

當一個人再開發程式的時候,寫的程式碼自己看得懂就好
但多人開發的時候,你的程式碼不只要給自己看,別人也要看
所以有些公司比較要求會有Code Review,來確保持程式碼有一定的水準

怎樣的程式不好閱讀?我覺得是以下兩個最常發生

  • 亂命名
  • 方法超過50行

其實我覺得當一個方法超過50行,裡面又包含上百行if else
這種程式碼看起來就相當累,也是在浪費生命
就如以下範例假設有一個列表的頁面需要輸入很多關鍵字來進行搜尋
所以搜尋頁面會有以下的程式

namespace Example.Controllers
{
    public class OrderController : Controller
    {
        // GET: Order
        public ActionResult Index(OrderArgs args)
        {
            using(var db = new NORTHWNDEntities())
            {
                var query = db.Orders.AsQueryable();

                //搜尋CustomerID
                if (string.IsNullOrEmpty(args.CustomerID))
                    query = query.Where(x => x.CustomerID.Contains(args.CustomerID));

                //搜尋OrderDate
                if (args.OrderStartDate != null && args.OrderEndDate != null)
                    query = query.Where(x => x.OrderDate >= args.OrderStartDate && x.OrderDate <= args.OrderEndDate);

                //搜尋ShipName
                if (string.IsNullOrEmpty(args.ShipName))
                    query = query.Where(x => x.ShipName.Contains(args.ShipName));

                //搜尋ShipAddress
                if (string.IsNullOrEmpty(args.ShipAddress))
                    query = query.Where(x => x.ShipAddress.Contains(args.ShipAddress));

                //搜尋ShipCity
                if (string.IsNullOrEmpty(args.ShipCity))
                    query = query.Where(x => x.ShipCity.Contains(args.ShipCity));

                return View(query.ToList());
            }            
        }
    }
}

這段Code在寫若有關鍵字傳進來,就對這個關鍵字做搜尋
其實這樣有註解、沒有複雜的邏輯也算還滿容易看得懂
那如果每個關鍵字裡面又有很多邏輯要判斷就會變成以下程式碼

namespace Example.Controllers
{
    public class OrderController : Controller
    {
        // GET: Order
        public ActionResult Index(OrderArgs args)
        {
            using(var db = new NORTHWNDEntities())
            {
                var query = db.Orders.AsQueryable();

                //搜尋CustomerID
                if (string.IsNullOrEmpty(args.CustomerID))
                {
                    /*
                    超多邏輯判斷
                    ............
                    ............
                    */
                    query = query.Where(x => x.CustomerID.Contains(args.CustomerID));
                }
                    

                //搜尋OrderDate
                if (args.OrderStartDate != null && args.OrderEndDate != null)
                {
                    /*
                    超多邏輯判斷
                    ............
                    ............
                    */
                    query = query.Where(x => x.OrderDate >= args.OrderStartDate && x.OrderDate <= args.OrderEndDate);
                }
                

                //搜尋ShipName
                if (string.IsNullOrEmpty(args.ShipName))
                {
                    /*
                    超多邏輯判斷
                    ............
                    ............
                    */
                    query = query.Where(x => x.ShipName.Contains(args.ShipName));
                }
                

                //搜尋ShipAddress
                if (string.IsNullOrEmpty(args.ShipAddress))
                {
                    /*
                    超多邏輯判斷
                    ............
                    ............
                    */
                    query = query.Where(x => x.ShipAddress.Contains(args.ShipAddress));
                }
                    

                //搜尋ShipCity
                if (string.IsNullOrEmpty(args.ShipCity))
                {
                    /*
                    超多邏輯判斷
                    ............
                    ............
                    */
                    query = query.Where(x => x.ShipCity.Contains(args.ShipCity));
                }                    

                return View(query.ToList());
            }            
        }
    }
}

是不是開始感覺有點雜亂了,當你這樣寫出來後面要修改你程式的人
又在加一些邏輯上去,一個方法破百行的成就就解開了
解開之後當然要繼續解破千行的成就= =,要如何讓他好閱讀呢
首先我把各種搜尋關鍵字寫成擴展,讓各種關鍵字複雜的邏輯切開,關注點分離
將每個複雜的搜尋都放在各別的擴展方法裡面

namespace Example.Models
{
    public static class OrderExtensions
    {
        public static IQueryable<Order> WhereByCustomerID(this IQueryable<Order> order, string customerID)
        {

            if (string.IsNullOrEmpty(customerID))
                return order;
            /*
            超多邏輯判斷
            ............
            ............
            */
            return order.Where(x => x.CustomerID.Contains(customerID));
        }

        public static IQueryable<Order> WhereByOrderDate(this IQueryable<Order> order, DateTime? StartDate, DateTime? EndDate)
        {

            if (StartDate == null || EndDate == null)
                return order;
            /*
            超多邏輯判斷
            ............
            ............
            */
            return order.Where(x => x.OrderDate >= StartDate && x.OrderDate <= EndDate);
        }

        public static IQueryable<Order> WhereByShipName(this IQueryable<Order> order, string ShipName)
        {

            if (string.IsNullOrEmpty(ShipName))
                return order;
            /*
            超多邏輯判斷
            ............
            ............
            */
            return order.Where(x => x.ShipName.Contains(ShipName));
        }

        public static IQueryable<Order> WhereByShipAddress(this IQueryable<Order> order, string ShipAddress)
        {

            if (string.IsNullOrEmpty(ShipAddress))
                return order;
            /*
            超多邏輯判斷
            ............
            ............
            */
            return order.Where(x => x.ShipAddress.Contains(ShipAddress));
        }

        public static IQueryable<Order> WhereByShipCity(this IQueryable<Order> order, string ShipCity)
        {

            if (string.IsNullOrEmpty(ShipCity))
                return order;
            /*
            超多邏輯判斷
            ............
            ............
            */
            return order.Where(x => x.ShipCity.Contains(ShipCity));
        }
    }
}

所以Controller的程式碼變得很有感覺,就算不寫註解您們看得懂嗎?

namespace Example.Controllers
{
    public class OrderController : Controller
    {
        // GET: Order
        public ActionResult Index(OrderArgs args)
        {
            using(var db = new NORTHWNDEntities())
            {
                var query = db.Orders.AsQueryable()
                    .WhereByCustomerID(args.CustomerID)
                    .WhereByOrderDate(args.OrderStartDate, args.OrderEndDate)
                    .WhereByShipName(args.ShipName)
                    .WhereByShipAddress(args.ShipAddress)
                    .WhereByShipCity(args.ShipCity);
                return View(query.ToList());
            }            
        }
    }
}

參考網址:
http://www.cnblogs.com/leotsai/p/how-to-write-beautiful-query-code.html

2017年12月6日 星期三

Entity Framework IEnumerable與IQueryable差異比較

若您有在使用Entity Framework的話,你去面試很多面試官都會問
IEnumerable與IQueryable的差別,我已經被問過N次了

IEnumerable:

取出當前符合條件的所有資料後,在記憶體中進行後續的資料篩選

IQueryable:

會保存所有條件限制於Query Expression中,直到資料真正被列舉成具體資料的當下,才將最終的Query Expression透過Query Provider轉換為實際執行的SQL語法,從DB取出符合條件資料。

講白話一點就是,你下同樣的條件式

var result = Table.AsEnumerable().Where(x=> x.ID == 1).ToList();
var result = Table.AsQueryable().Where(x=> x.ID == 1).ToList();

上面兩行取出來的結果是一樣的,但是取的過程完全不一樣

IEnumerable會在您ToList的時候先取出Table的所有資料存到記憶體中
再去從記憶體中的資料下條件式(where)取出
也就是你Table有10萬筆資料,他會先把10萬筆存到記憶體中,再去撈ID=1得到的結果
所以你Table有上百萬筆,千萬別用IEnumerable來取資料您會GG

IQueryable會在您ToList的時候,他會包含WHERE條件式送給SQL SERVER取要得資料

適用時機

IEnumerable: 處理記憶體內的資料 ex. List / Array
IQueryable: 處理遠端來源的資料 ex. Database / Service


參考網址:
https://dotblogs.com.tw/wasichris/archive/2015/03/04/150633.aspx



2017年12月5日 星期二

C# 委派 delegate

C# 的委派好像都很少用到,不過最近看同事寫一個驗證資料的東西
好像不錯用...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static StringBuilder result = new StringBuilder();
        private delegate void Verification<T>(T data);
        static void Main(string[] args)
        {            
            string data = "xxxx";//要驗證的資料

            Verification<string> verif;
            verif = CheckName;
            verif += CheckEmail;
            verif += CheckAddress;
            verif(data);

            Console.WriteLine(result.ToString());
            Console.ReadLine();
        }

        private static void CheckName(string data)
        {
            result.AppendLine("驗證姓名");
        }
        private static void CheckEmail(string data)
        {
            result.AppendLine("驗證電子信箱");
        }
        private static void CheckAddress(string data)
        {
            result.AppendLine("驗證地址");
        }
    }
}


其實丟進去都是同一個物件
要從這一個物件判斷很多驗證,使用委派就挺不錯的!!

2017年11月28日 星期二

Entity Framework 無法更新 EntitySet 'customer_payment_type',因為它有 DefiningQuery

當我再改公司一個Ticket時,我更新了一個資料庫的Entity Framework
更新完之後發生了一個錯誤

無法更新 EntitySet 'customer_payment_type',因為它有 DefiningQuery,但是在 <ModificationFunctionMapping> 項目中沒有 <InsertFunction> 項目來支援目前的作業

查了一下Google是資料表沒有索引
所以如果將資料表加上索引重新更新EF就可以解決這一個問題

但是呢這一個資料表在某些原因下無法加入索引
所以該怎麼解決呢
  1. 右鍵點擊edmx文件,選擇Open with,XML編輯器
  2. 在edmx:StorageModels元素中找到實體
  3. 完全刪除DefiningQuery
  4. 重命名store:Schema="dbo"為Schema="dbo"(否則,代碼將生成一個錯誤,說名稱是無效的)
所以簡單說呢就是把edmx打開XML編輯模式


<EntitySet Name="customer_payment_type" EntityType="Self.customer_payment_type"
store:Type="Tables" store:Schema="dbo">
<DefiningQuery>SELECT 
    [customer_payment_type].[customer_id] AS [customer_id], 
    [customer_payment_type].[customer_payment_type_no] AS [customer_payment_type_no], 
    [customer_payment_type].[payment_type] AS [payment_type], 
    [customer_payment_type].[payment_condition] AS [payment_condition], 
    [customer_payment_type].[period_days] AS [period_days], 
    [customer_payment_type].[closed_date] AS [closed_date], 
    [customer_payment_type].[description] AS [description], 
    [customer_payment_type].[remark] AS [remark], 
    [customer_payment_type].[default_value] AS [default_value], 
    [customer_payment_type].[credit_currency] AS [credit_currency], 
    [customer_payment_type].[credit_amount] AS [credit_amount], 
    [customer_payment_type].[createdby] AS [createdby], 
    [customer_payment_type].[createdtime] AS [createdtime], 
    [customer_payment_type].[revisedby] AS [revisedby], 
    [customer_payment_type].[revisedtime] AS [revisedtime], 
    [customer_payment_type].[applyby] AS [applyby], 
    [customer_payment_type].[applytime] AS [applytime], 
    [customer_payment_type].[approvedby] AS [approvedby], 
    [customer_payment_type].[approvedtime] AS [approvedtime], 
    [customer_payment_type].[workflowstate] AS [workflowstate], 
    [customer_payment_type].[workflowstate_remark] AS [workflowstate_remark], 
    [customer_payment_type].[rowstate] AS [rowstate], 
    [customer_payment_type].[update_remark] AS [update_remark], 
    [customer_payment_type].[update_flag] AS [update_flag]
    FROM [dbo].[customer_payment_type] AS [customer_payment_type]
</DefiningQuery>
</EntitySet>
更新成
<EntitySet Name="customer_payment_type" EntityType="Self.customer_payment_type"
Schema="dbo" store:Type="Tables" /> 
收工...

2017年11月17日 星期五

遞迴父子階層

遞迴除了在面試考題很常出之外,最常應用的應該就是在撈父子階層了
假設有一個資料表如下
| Id | Name    | ParentId |
|-------------------------|
| 1  | Ricky   | 2        |
| 2  | Jackie  | 3        |
| 3  | Peter   | 4        |
| 4  | Anson   | 5        |
| 5  | Austin  | 0        |

ParentID是主管的ID,所以這張表的意思就是

Ricky 的主管是 Jackie 
Jackie 的主管是 Peter 
Peter 的主管是 Anson 
Anson 的主管是 Austin 

這時候我想撈Ricky所有的上司,就可以使用遞迴
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    class Program
    {
        private static List _user = new List();            
        static void Main(string[] args)
        {
            _user.Add(new User { Id = 1, Name = "Ricky", ParentID = 2 });
            _user.Add(new User { Id = 2, Name = "Jackie", ParentID = 3 });
            _user.Add(new User { Id = 3, Name = "Peter", ParentID = 4 });
            _user.Add(new User { Id = 4, Name = "Anson", ParentID = 5 });
            _user.Add(new User { Id = 5, Name = "Austin", ParentID = 0 });            
            
            factorial(_user.Where(x=>x.Name=="Ricky").FirstOrDefault());//抓取Ricky上面所有的主管
            Console.ReadLine();
        }

        //遞迴抓取向上所有主管
        private static void factorial (User args)
        {
            var result = _user.Where(x => x.Id == args.ParentID).FirstOrDefault();
            if(result != null)
            {
                Console.WriteLine(string.Format("{0}的主管是{1}", args.Name, result.Name));
                factorial(result);
            }            
        }

        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int ParentID { get; set; }
        }
    }
}
結果如下

2017年11月16日 星期四

Outlook EML 或 MHT 取出信件內容與圖片

最近公司有一個需求,他們要從Outlook另存新檔成EML或MHT檔案
然後再把EML讀出來,再轉寄給其他使用者
把EML信件內文讀出來很容易,有很多元件可以使用
但是若是EML中含有圖片,你把內文讀取出來轉寄給其他使用者時圖片是會破圖的
以下是EML格式
From 
From: from@example.com
To: to@example.com
Subject: HTML Messages with Embedded Pic in Signature
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="alternative_boundary"

This is a message with multiple parts in MIME format.

--alternative_boundary
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

test

-- 
[Picture of a Christmas Tree]

--alternative_boundary
Content-Type: multipart/related; boundary="related_boundary"

--related_boundary
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <p>test</p>
        <p class="sig">-- <br><img src="cid:0123456789"></p>
    </body>
</html>

--related_boundary
Content-Type: image/png; name="sig.png"
Content-Disposition: inline; filename="sig.png"
Content-Location: sig.png
Content-ID: <0123456789>
Content-Transfer-Encoding: base64

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
cHkgSG9saWRheXMgUGFnZQA7

--related_boundary--

--alternative_boundary--
之中的這一段就是信件的內文,使用許多元件(CDO Message)可以取出這樣的信件內容
參考這裡 https://github.com/riesvriend/CDO-EML-Parsing-Sample
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <p>test</p>
        <p class="sig">-- <br><img src="cid:0123456789"></p>
    </body>
</html>
這邊可以看出內文含有圖片,但是你把這一段內文送給StmpClient去寄送,圖片是無法顯示的
原因就是Outlook會把圖像作為MIME附件包含在電子郵件中,每個附件都必須有一個“Content-ID”標題
所以圖片只要指定CID就可以顯示出圖片,這也就是為什麼EML裡面會含有圖片的base64

所以我只取出信件內文是不夠的,我還要把EML裡面圖片的Base64一一取出
至於image Base64怎麼抓,我是直接抓取EML字串的
再使用LinkedResource加回Outlook中

string emailReadyHtml = string.empty;
emailReadyHtml += "<p>Hello World, below are two embedded images : </p>";
emailReadyHtml += "<img src=\"cid:yasser\" >";
emailReadyHtml += "<img src=\"cid:smile\" >";
MailMessage mailMessage = new MailMessage();

mailMessage.To.Add("yasser@mail.yy");
mailMessage.From = new MailAddress("info@mail.yy", "Info");

mailMessage.Subject = "Test Mail";
mailMessage.IsBodyHtml = true;

string image1Path = HttpContext.Current.Server.MapPath("~/Content/images/yasser.jpg");
byte[] image2Bytes = someArrayOfByte;

ContentType c = new ContentType("image/jpeg");

LinkedResource linkedResource1 = new LinkedResource(imagePath);
linkedResource1.ContentType = c ;
linkedResource1.ContentId = "yasser";
linkedResource1.TransferEncoding = TransferEncoding.Base64;

LinkedResource linkedResource2 = new LinkedResource(new MemoryStream(image2Bytes));
linkedResource2.ContentType = c;
linkedResource2.ContentId = "smile";
linkedResource2.TransferEncoding = TransferEncoding.Base64;

AlternateView alternativeView = 
  AlternateView.CreateAlternateViewFromString(emailReadyHtml, null, MediaTypeNames.Text.Html);

alternativeView.ContentId = "htmlView";
alternativeView.TransferEncoding = TransferEncoding.SevenBit;

alternativeView.LinkedResources.Add(linkedResource1) ;
alternativeView.LinkedResources.Add(linkedResource2);

mailMessage.AlternateViews.Add(alternativeView);

SmtpClient smtpClient = new SmtpClient();
smtpClient.Send(mailMessage);
這真的是有點麻煩,也搞了我好幾天

2017年10月31日 星期二

SQL GROUP BY MAX() 取所有欄位

假設有一個資料表如下
| Id | Message | GroupId | Date |
|-------------------------------|
| 1  | Hello   | 1       | 1:00 |
| 2  | Hello   | 1       | 1:01 |
| 3  | Hey     | 2       | 2:00 |
| 4  | Dude    | 3       | 3:00 |
| 5  | Dude    | 3       | 3:01 |
但我想撈取的結果如下
| Id | Message | GroupId | Date |
|-------------------------------|
| 1  | Hello   | 1       | 1:00 |
| 3  | Hey     | 2       | 2:00 |
| 4  | Dude    | 3       | 3:00 |
也就是想要Message不重複,然後Date取最小值得的,一般會這樣下SQL
SELECT Message,MIN(Date) FROM GROUP BY Message

來取得這樣的結果,但是我想取到所有的欄位該如何下SQL呢

方法一: 使用EF LINQ
var result = data.GroupBy(item => item.GroupId)
                 .Select(grouping => grouping.FirstOrDefault())
                 .OrderByDescending(item => item.Date)
                 .ToList();

或
var result = data.GroupBy(item => item.GroupId)
                 .SelectMany(grouping => grouping.Take(1))
                 .OrderByDescending(item => item.Date)
                 .ToList();

若想使用Order by
var result = data.GroupBy(item => item.GroupId)
                 .SelectMany(grouping => grouping.OrderBy(item => item.Date).Take(1))
                 .OrderByDescending(item => item.Date)
                 .ToList();
方法二: 使用SQL CTE
WITH CTE AS
(
    SELECT GroupId, MIN(Date) as MinDate
    FROM table
    GROUP BY GroupId
)
SELECT
    T.*
FROM table AS T
JOIN CTE AS C on C.GroupId = T.GroupId AND C.MinDate=T.Date

Entity Framework的SaveChanges有使用Transaction

在新公司常看到Batch Console跳出資料表被鎖定的例外錯誤
造成其他的程式無法讀取該資料表
經查詢發現某支Console常常做一些EF跑上千筆的更新
然後上網查了一下資料,原來EF 的SaveChanges有在使用Transaction
所以還是別一次跑上千筆的更新比較恰當
而且EF的RemoveRange在SQL上是一筆一筆exec去跑的,我也是醉了



圖片來源:https://dotblogs.com.tw/yc421206/2015/03/18/150757

一個成功的Git分支模型圖


2017年5月23日 星期二

ASP.NET MVC Entity Framework 使用 Repository & Unit of Work

Entity Framework其實本身就是一個Repository
也都有CRUD(新增、修改、讀取、刪除)的操作,也有Transaction的操作

那為什麼要再給它包一層Repository來操作EF呢?
然後再用Unit of Work來做Transaction呢?

目前我想到把EF包成Repository的最大優點應該是
日後變換ORM或其他資料庫時無需變更上層的程式,也就不跟EF綁得死死的
在單元測試的時候來源資料切換容易

但是仔細想想,一個專案要變更資料庫的機會大嗎?
我目前是還沒遇到過,所以EF是否在套上一個Repository就見人見智了

那回歸正題,Entity Framework 如何使用 Repository & Unit of Work
首先先設計Repository的CRUD類別

Repository介面的定義

IRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace enjoy01coding.Repository
{
    public interface IRepository<TEntity> where TEntity : class
    {
        IEnumerable<TEntity> Get(
        Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
        string includeProperties = "");

        TEntity GetByID(object id);
        void Insert(TEntity entity);
        void Delete(object id);
        void Delete(TEntity entityToDelete);
        void Update(TEntity entityToUpdate);

    }
}

Repository介面的實作

Repository.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace enjoy01coding.Repository
{
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        internal DbContext context;
        internal DbSet<TEntity> dbSet;

        public Repository(DbContext context)
        {
            this.context = context;
            this.dbSet = context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = dbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }
        public virtual TEntity GetByID(params object[] id)
        {
            return dbSet.Find(id);
        }
        public virtual TEntity GetByID(object id)
        {
            return dbSet.Find(id);
        }

        public virtual void Insert(TEntity entity)
        {
            dbSet.Add(entity);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = dbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    }
}

但是呢Repository一次只能針對一個Table來進行操作
如果我有兩個或多個Table要操作
而且只要有一個Table操作失敗,此次操作就應該要算全部失敗
因為有時候不允許A Table已經成功寫入資料,但是B C Table都寫入失敗
就是要寫一種Transaction的概念,它就是Unit of work

這時候要在建立UnitOfWork類別

UnitOfWork介面的定義

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace enjoy01coding.Repository
{
    public interface IUnitOfWork : IDisposable
    {
        IRepository<T> GetRepository<T>() where T : class;
        void Save();
        void Dispose();
    }
}

UnitOfWork介面的實作

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace enjoy01coding.Repository
{
    public class UnitOfWork<C> : IDisposable, IUnitOfWork where C : DbContext, new()
    {
        private C context = new C();
        private Hashtable repositories = new Hashtable();
        public IRepository<T> GetRepository<T>() where T : class
        {
            if (!repositories.Contains(typeof(T)))
            {
                repositories.Add(typeof(T), new Repository<T>(context));
            }
            return (IRepository<T>)repositories[typeof(T)];
        }
        public void Save()
        {
            context.SaveChanges();
        }
        private bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

該如何使用呢?我這裡直接寫在Controllers展示

using enjoy01coding.NorthwindDB;
using enjoy01coding.Repository;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace enjoy01coding.Controllers
{
    public class HomeController : Controller
    {
        IUnitOfWork unitOfWork;

        //Autofac DI 注入
        public HomeController(IUnitOfWork inunitOfWork)
        {
            this.unitOfWork = inunitOfWork;
        }

        public ActionResult Index()
        {
            ViewBag.Message = "";

            //新增Categories資料測試
            NorthwindDB.Categories CategoriesData = new NorthwindDB.Categories
            {
                CategoryName = "Categories TEST",
                Description = "Categories TEST"
            };

            //新增Region資料測試
            NorthwindDB.Region RegionData = new NorthwindDB.Region
            {
                RegionID = 1,
                RegionDescription = "Region TEST"
            };

            try
            {           
                var categories = unitOfWork.GetRepository<NorthwindDB.Categories>();
                categories.Insert(CategoriesData);

                var region = unitOfWork.GetRepository<NorthwindDB.Region>();
                region.Insert(RegionData);

                unitOfWork.Save();
                ViewBag.Message = "OK.";

            }
            catch(Exception ex)
            {
                ViewBag.Message = "ERROR. " + ex.Message;
            }

            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "";
            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "";

            return View();
        }
    }
}

以上就是Entity Framework 使用 Repository & Unit of Work
若對分層式架構有興趣可參考以下連結

Alan Tsai 的學習筆記
http://blog.alantsai.net/2014/10/BuildYourOwnApplicationFrameworkOnMvc-30-Conclusion.html#WizKMOutline_1414590577032885

mrkt 的程式學習筆記
http://kevintsengtw.blogspot.tw/2015/04/aspnet-mvc-twmvc18.html