[.NET][C#][Design Pattern] - Repository Pattern

前言

這篇文章主要介紹何為Repository Pattern, 並如何在.NET C#中實作
並結合Unit Of Work Pattern, 讓程式對Repository解耦
這個design pattern可以說是踏入軟體架構的敲門磚
也算是非常常用的pattern, 實用性非常高

定義:Repository Pattern, Unit Of Work Pattern

IG貼文
Repository Pattern Post

GitHub連結:
Sample Code

Repository Pattern主要就是由Repository這個元素所組成
我個人都會結合Unit Of Work Pattern一起使用

[Repository] :
Act like a collection of object in memory.

[Unit of work] :
Maintain a list of objects affected by a business transaction and coordinate th writting out of changes.

以上是對兩個主要元素的定義
簡單來說 Repository就像是一群存在記憶體中的Objects
Unit of work則是針對這些被改變的Objects, 統一在這裡做處理

更簡單的解釋

UnitOfWork在這裡就像是DB, Repository就像是Table.
如果以EntityFramework來看, 就是Context和Entity的關係.
EF本身也有做Repository和UnitOfWork.

把每一次的operation看作一個unit of work, 等到operation結束才Complete.
這樣一來, 當我們的Controller或Service需要很多Repository來操作時, 就只需要依賴UnitOfWork, 簡化程式碼與依賴.
另一個好處是可以管理Repository之間之間的情況, 做到DB的Atomic operation.
舉例來說, 當Repository1儲存成功, 而Repository2儲存失敗, 以一個完整的Atomic operation來說, 一個失敗, operation就算是失敗.
但在這裡, Repository之間是沒有聯繫的, 因此資料會處於一種dirty state, 就是一個進去, 但一個失敗了.
沒辦法回到最初的狀況再debug, UnitOfWork就是來處理這種問題, 當Complete時失敗, 並不會真的動到DB, 而是只動到Repository. 可以整個roll back處理

補充:Atomic operation是資料庫ACID四個特性中的其中一個, 而使用Transaction(交易), 就是實現Atomicity的方法之一, 簡單一句話來說就是「全有,或全無」

好處

  1. 使用Repository主要是可以對ORM的框架解耦, 對Context和Entities的依賴度不會太高.
    舉例來說我今天使用Entity Framework, 開發了幾個月, 老闆突然要使用ADO.NET
    這時如果沒有使用Repository, 而是在程式碼裡面大量使用Context與Entity
    那麼要修改程式碼就是一件大工程了….
  2. 使用Unit Of Work Pattern, 可以統一管理Repository的變動, 再統一Save到Database,
    這麼做可以確保每一次的Operation為全有或全無, 避免資料庫處於一個Dirty State
  3. 下面引用我在IG上的照片, 裡面還有提到一個好處是更好做單元測試.

不管是No-SQL或RDB都可以套用此模式, 但MongoDB需要另外建立Replica-Set


可以看到, repository是存放在unit of work內, 我們的程式是去依賴unit of work,
unit of work再透過repository去操作ORM, ORM再去操作我們的資料庫.

實作

先附上UML

稍微解釋一下, 這裡會有一個Generic Repository, 負責處理基本的Get, Find, Romove等.
而其他Concrete Repository再去繼承和實作, 內部就針對自己的Repository做特殊的Query或Command.

接下來會使用.NET去實作裡面的各個細節
先附上專案結構和ERD

這裡主要是以書本和作者為範例

Models裡面是用EntityFramework migration過來的Context和Entities model
我們主要實作Repository和UnitOfWork的部分

IRepository, Repository

這裡是generic的repository

IRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;
using System.Linq.Expressions;

namespace RepositoryPatternPractice.Repositories
{
//Interface here act like a protocol or a license
//Generic Interface -> Interface<T>
//where keyword can set some restriction on the generic
public interface IRepository<TEntity> where TEntity: class
{
//Three main group of functions

//Finding objects
TEntity Get(int id);
IEnumerable<TEntity> GetAll();
IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);

/*
Func<input, output> -> 委派物件(將函數當作物件的容器)
eg. Func<int, int> fn = n=>n*n;
Expression (eg. LINQ)
turn an lambda to an expression tree
and the LINQ can input a lambda expression (put in a generic delegate)
*/

//Adding object
void Add(TEntity entity);
void AddRange(IEnumerable<TEntity> entities);

//Removing object
void Remove(TEntity entity);
void RemoveRange(IEnumerable<TEntity> entities);
}
}

!! 要注意的是, Repository不應該有直接Update或是Save Database的方法, 主要是語義問題. !!
Repository act like a collection of objects in memory.
這些改變Database的動作, 應該交由UnitOfWork, 把這些objects save到database.
所以應該是UnitOfWork透過Repository撈出來, 然後修改, 再透過UnitOfWork Save.
可以使用Transaction機制, 將Save Changes的動作統一執行, 這樣一來才可以確保Atomic Operation


Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;

namespace RepositoryPatternPractice.Repositories
{
public class Repository<TEntity> : IRepository<TEntity> where TEntity: class
{
//the context here is generic, so it has nothing to do with my application
//so you can DI some specific contexts
//protected because the specific repository can use it
protected readonly DbContext Context;

public Repository(DbContext context)
{
Context = context;
}

public TEntity Get(int id)
{
return Context.Set<TEntity>().Find(id);
}

//don't return IQueryable!!
//Repository should encapsulate the query
//so on the Service or Controller won't get too much pressure
public IEnumerable<TEntity> GetAll()
{
return Context.Set<TEntity>().ToList();
}

public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
{
return Context.Set<TEntity>().Where(predicate);
}

public void Add(TEntity entity)
{
Context.Set<TEntity>().Add(entity);
}

public void AddRange(IEnumerable<TEntity> entities)
{
Context.Set<TEntity>().AddRange(entities);
}

public void Remove(TEntity entity)
{
Context.Set<TEntity>().Remove(entity);
}

public void RemoveRange(IEnumerable<TEntity> entities)
{
Context.Set<TEntity>().RemoveRange(entities);
}
}
}

IBookRepository, BookRepository

這裡開始是concrete的repository

IBookRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using RepositoryPatternPractice.Models;

namespace RepositoryPatternPractice.Repositories
{
//derive from my generic Repository interface
//C# allow this interface chain
public interface IBookRepository : IRepository<Book>
{
IEnumerable<Book> GetTopSellingBooks(int count);
IEnumerable<Book> GetBooksByAuthor(Author author);
}
}
BookRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using RepositoryPatternPractice.Models;

namespace RepositoryPatternPractice.Repositories
{
//inheritance, implementation
public class BookRepository : Repository<Book>, IBookRepository
{
//Property
public MypostgresContext MypostgresContext
{
get { return Context as MypostgresContext; }
}

public BookRepository(MypostgresContext context) : base(context)
{
}

IEnumerable<Book> IBookRepository.GetBooksByAuthor(Author author)
{
return MypostgresContext.Books.Where(x=>x.AuthorId==author.AuthorId).ToList();
}

IEnumerable<Book> IBookRepository.GetTopSellingBooks(int count)
{
return MypostgresContext.Books.OrderByDescending(b => b.Price).Take(count).ToList();
}
}
}

IAuthorRepository, AuthorRepository

IAuthorRepository
1
2
3
4
5
6
7
8
9
10
using System;
using RepositoryPatternPractice.Models;

namespace RepositoryPatternPractice.Repositories
{
public interface IAuthorRepository : IRepository<Author>
{
Author GetAuthorByName(string name);
}
}
AuthorRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using RepositoryPatternPractice.Models;

namespace RepositoryPatternPractice.Repositories
{
public class AuthorRepository : Repository<Author>, IAuthorRepository
{
public MypostgresContext MypostgresContext
{
get { return Context as MypostgresContext; }
}

public AuthorRepository(MypostgresContext context) : base(context)
{
}

public Author GetAuthorByName(string name)
{
return MypostgresContext.Authors.Where(x=>x.AuthorName.Equals(name)).ToList().FirstOrDefault();
}
}
}

IUnitOfWork, UnitOfWork

這裡開始是unit of work的實作

IUnitOfWork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using RepositoryPatternPractice.Repositories;

namespace RepositoryPatternPractice
{
//Unit of work
//interfce chain with IDisposable, so the class implement this interface
//need to implement the Dispose() method
public interface IUnitOfWork : IDisposable
{
//Repository act the collection of objects in memory
IBookRepository Books { get; }
IAuthorRepository Authors { get; }
int Complete();
}
}
UnitOfWork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using RepositoryPatternPractice.Models;
using RepositoryPatternPractice.Repositories;

namespace RepositoryPatternPractice
{
public class UnitOfWork : IUnitOfWork
{
private readonly MypostgresContext _context;

public IBookRepository Books { get; private set; }
public IAuthorRepository Authors { get; private set; }

//we will use this context across all repositories
public UnitOfWork(MypostgresContext context)
{
this._context = context;
//use the same context to initialize our repository
Books = new BookRepository(_context);
Authors = new AuthorRepository(_context);
}

public int Complete()
{
return this._context.SaveChanges();
}

public void Dispose()
{
_context.Dispose(); //dispose the context
}
}
}

這裡可以發現, UnitOfWork將Repository作為自己的屬性, 這樣一來使用者就可以透過UnitOfWork點出Repository再做操作.
而Complete()方法, 就是拿來Save用的, 儲存所有異動
最後實作Dispose是因為可以透過using block釋放context
在每一次的operation, Repository應該使用同一個context (re-use)
但如果你有使用DI框架的話, 可以直接將Unit of work DI進去, 就不用使用using block來去釋放context. (主要是管理Transaction的session要釋放)

使用

這裡是使用範例

使用範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
using RepositoryPatternPractice.Models;

namespace RepositoryPatternPractice
{
class Program
{
public static void Main(string[] args)
{
//example of using these interfaces and classes
//using block like try/finally and call Dispose()
using ( var unitOfWork = new UnitOfWork(new MypostgresContext()) )
{
//Example 1
var books = unitOfWork.Books.GetAll();
Console.WriteLine("Initial State: ");
foreach (Book b in books)
{
Console.WriteLine($"book: {b.BookName}, author: {unitOfWork.Authors.Get(b.AuthorId).AuthorName}");
}
Console.WriteLine();

//Example 2
unitOfWork.Books.AddRange(new List<Book>() {
new Book() { BookName="AIGuide", Price=300, Author=unitOfWork.Authors.GetAuthorByName("Xuan")},
new Book() { BookName="PSGuide", Price=230, Author=unitOfWork.Authors.GetAuthorByName("Xuan")}
});


//Example3
unitOfWork.Books.Get(2).BookName = "C#dotNETGuide";

unitOfWork.Complete();

Console.WriteLine("After changes: ");
books = unitOfWork.Books.GetAll();
foreach (Book b in books)
{
Console.WriteLine($"book: {b.BookName}, author: {unitOfWork.Authors.Get(b.AuthorId).AuthorName}");
}
Console.WriteLine();
}
Console.ReadKey();
}
}
}

結語

如果想要做到非同步, 只要把Repository內部的實作改成非同步就好, 其餘都一樣.
其實Repository pattern, Unit of Work pattern很常會一起使用, 甚至搭配DI, 但這裡為求簡單就沒有使用DI了
希望大家看完這篇文章對於Repository Pattern和UnitOfWork Pattern更加理解