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

1 則留言:

  1. 為什麼UnitOfWork要繼承IDisposable呢,我看你的程式碼裡面也有沒用到釋放資源的方法,挺好奇的

    回覆刪除