隨手 Design Pattern (4) - Repository 模式 (Repository Pattern)

Repository 模式與 UnitOfWork 模式可說是充滿爭議,國外大神爭論不休,本篇不打算加入筆戰,只簡單提供幾個適合的應用的情境與範例,大家就自行選擇消化囉。

Repository 模式的好處

在軟體分層那篇文章中,資料存取層的實作便是 Repository 模式的體現,有效的將資料存取隔離於商業邏輯之外,當要抽換資料來源時,無須改動展示層與商業邏輯層(前提是 Repoistory 約定的介面沒有變動)。

專用型 IRepository 與 泛型 IRepository

我們先定義兩種應用的模式

  • 專用型 Repository:一個 interface 實作 一個 Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IBlogRepository
{
// ....
}

public class BlogRepository : IBlogRepository
{
// ....
}

public interface IPostRepository
{
// ....
}

public class PostRepository : IPostRepository
{
// ....
}
  • 泛型 Repository:一個 interface 實作 一個 Repository,透過不同的 TEntity 來操作不同的資料表。
1
2
3
4
5
6
7
8
9
public interface IGenericRepository<TEntity>
{
// ....
}

public class GenericRepository : GenericRepository<TEntity>
{
// ....
}

當 Repository 有不同的介面方法的時候,專用型 Repository 能提供最大的彈性。

當 Repoitory 有固定的 CRUD 的介面方法,泛型 Repository 可以有效的減少重複的程式碼。

專用型 Repository 模式 with Daaper

而該架構對於 Dapper 這類輕量型 ORM 可以說搭配的如魚得水,因為 Dapper 執行我們定義好的 sqlcommand,專注在強型別的操作,故對於 Repository 的介面
並無直接的對應操作,也讓程式設計師有了較大的彈性去定義介面的方法,下面程式範例是專用型的寫法,改成泛型也是可以的喔。

DapperHelper

使用 Dapper 建議作個 DapperHelper 來隔離。

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
/// <summary>
/// Database 介面
/// </summary>
public interface IDatabaseHelper
{
/// <summary>
/// 取得連線
/// </summary>
/// <returns></returns>
IDbConnection GetConnection();
}

/// <summary>
/// DatabaseHelper
/// </summary>
/// <seealso cref="Sample.Repository.Infrastructure.IDatabaseHelper" />
public class DatabaseHelper : IDatabaseHelper
{
private readonly string _connectionString;

/// <summary>
/// Initializes a new instance of the <see cref="DatabaseHelper"/> class.
/// </summary>
/// <param name="connectionString">The database.</param>
public DatabaseHelper(string connectionString)
{
this._connectionString = connectionString;
}

/// <summary>
/// 取得連線
/// </summary>
/// <returns></returns>
public IDbConnection GetConnection()
{
var conn = new SqlConnection(this._connectionString);

return conn;
}
}

Respository

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
/// <summary>
/// Interface BlogRepository
/// </summary>
public interface IBlogRepository
{
/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
Task<int> AddAsync(Blog blog);

/// <summary>
/// 取得所有 Blog
/// </summary>
/// <param name="blogQuery">查詢條件</param>
Task<IEnumerable<Blog>> GetAsync(BlogQuery blogQuery);

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
Task<int> RemoveAsync(Blog blog);

/// <summary>
/// 更新 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
Task<int> UpdateAsync(Blog blog);
}

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/// <summary>
/// BlogRepository
/// </summary>
public class BlogRepository : IBlogRepository
{
/// <summary>
/// The database
/// </summary>
private readonly IDatabaseHelper _databaseHelper;

/// <summary>
///
/// </summary>
/// <param name="databaseHelper"></param>
public BlogRepository(IDatabaseHelper databaseHelper)
{
this._databaseHelper = databaseHelper;
}

/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
/// <returns></returns>
public async Task<int> AddAsync(Blog blog)
{
// 資料庫實作
using (IDbConnection conn = this._databaseHelper.GetConnection())
{
string sql = @"
INSERT INTO Blog
VALUES (
@BlogId,
@Url
);";

var count = await conn.ExecuteAsync(
sql,
new
{
blog.BlogId,
blog.Url
});

return count;
}
}

/// <summary>
/// 取得所有 Blog
/// </summary>
/// <param name="blogQuery">查詢條件</param>
/// <returns></returns>
public async Task<IEnumerable<Blog>> GetAsync(BlogQuery blogQuery)
{
// 資料庫實作
using (IDbConnection conn = this._databaseHelper.GetConnection())
{
string sql = @"
SELECT
BlogId,
Url
FROM Blog
WHERE
BlogId = @BlogId OR
Url = @Url";

var Blogs = await conn.QueryAsync<Blog>(
sql,
new
{
blogQuery.BlogId,
blogQuery.Url
});

return Blogs;
}
}

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
/// <returns></returns>
public async Task<int> RemoveAsync(Blog blog)
{
// 資料庫實作
using (IDbConnection conn = this._databaseHelper.GetConnection())
{
string sql = @"DELETE FROM Blog WHERE BlogId = @BlogId";

var count = await conn.ExecuteAsync(
sql,
new
{
blog.BlogId
});

return count;
}
}

/// <summary>
/// 更新 Blog
/// </summary>
/// <param name="blog">Blog 實體</param>
/// <returns></returns>
public async Task<int> UpdateAsync(Blog blog)
{
// 資料庫實作
using (IDbConnection conn = this._databaseHelper.GetConnection())
{
string sql = @"
UPDATE Blog
SET
BlogId = @BlogId,
Url = @Url
);";

var count = await conn.ExecuteAsync(
sql,
new
{
blog.BlogId,
blog.Url
});

return count;
}
}
}

Service 的應用

注入 IBlogRepository 就可以使用。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/// <summary>
/// BlogService
/// </summary>
public class BlogService : IBlogService
{
private IBlogRepository _blogRepository;
private IMapper _mapper;

/// <summary>
/// Initializes a new instance of the <see cref="BlogService"/> class.
/// </summary>
public BlogService(
IBlogRepository blogRepository,
IMapper mapper)
{
this._blogRepository = blogRepository;
this._mapper = mapper;
}

/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> AddAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

return await this._blogRepository.AddAsync(blog); ;
}

/// <summary>
/// 取得 Blog
/// </summary>
/// <param name="blogQueryDto">查詢條件</param>
/// <returns></returns>
public async Task<IEnumerable<BlogDto>> GetAsync(BlogQueryDto blogQueryDto)
{
// Convert BlogQueryDto to BlogQuery
var blogQuery = this._mapper.Map<BlogQuery>(blogQueryDto);

var blogs = await this._blogRepository.GetAsync(blogQuery);

// Convert Blog to BlogDto
var blogDtos = this._mapper.Map<IEnumerable<BlogDto>>(blogs);

return blogDtos;
}

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="blogId">Blog Id</param>
/// <returns></returns>
public async Task<int> RemoveAsync(int blogId)
{
return await this._blogRepository.RemoveAsync(new Blog() { BlogId = blogId });
}

/// <summary>
/// 修改 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> UpdateAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

return await this._blogRepository.UpdateAsync(blog);
}
}

注入設定

Startup.cs 的注入設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
var connection = this.Configuration.GetConnectionString("DefaultConnection");

// DI Register
services.AddScoped<IBlogService, BlogService>();
services.AddScoped<IBlogRepository, BlogRepository>();
services.AddScoped<IDatabaseHelper>(x => new DatabaseHelper(connection));

///..............
}

專用型 Repository with Dapper 程式碼範例

專用型 Repository 模式 with EFCore

專用型 IRepository 搭配 EFCore 只需抽換 DapperHelper 的部分即可。

DBContext

EFCore 使用指定產生出 DBContext 如下

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/// <summary>
///
/// </summary>
/// <seealso cref="Microsoft.EntityFrameworkCore.DbContext" />
public partial class BloggingContext : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="BloggingContext"/> class.
/// </summary>
public BloggingContext()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="BloggingContext"/> class.
/// </summary>
/// <param name="options">The options.</param>
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{
}

/// <summary>
/// blog.
/// </summary>
public virtual DbSet<Blog> Blog { get; set; }

/// <summary>
/// post.
/// </summary>
public virtual DbSet<Post> Post { get; set; }

/// <summary>
/// <para>
/// Override this method to configure the database (and other options) to be used for this context.
/// This method is called for each instance of the context that is created.
/// The base implementation does nothing.
/// </para>
/// <para>
/// In situations where an instance of <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions" /> may or may not have been passed
/// to the constructor, you can use <see cref="P:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.IsConfigured" /> to determine if
/// the options have already been set, and skip some or all of the logic in
/// <see cref="M:Microsoft.EntityFrameworkCore.DbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)" />.
/// </para>
/// </summary>
/// <param name="optionsBuilder">A builder used to create or modify options for this context. Databases (and other extensions)
/// typically define extension methods on this object that allow you to configure the context.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}

/// <summary>
/// Override this method to further configure the model that was discovered by convention from the entity types
/// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached
/// and re-used for subsequent instances of your derived context.
/// </summary>
/// <param name="modelBuilder">The builder being used to construct the model for this context. Databases (and other extensions) typically
/// define extension methods on this object that allow you to configure aspects of the model that are specific
/// to a given database.</param>
/// <remarks>
/// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />)
/// then this method will not be run.
/// </remarks>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");

modelBuilder.Entity<Blog>(entity =>
{
entity.Property(e => e.Url).IsRequired();
});

modelBuilder.Entity<Post>(entity =>
{
entity.HasOne(d => d.Blog)
.WithMany(p => p.Post)
.HasForeignKey(d => d.BlogId);
});
}
}

Repository with DBContext 實作

Repository 介面與 Service 使用都不需修改,只需修改 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/// <summary>
/// Class BlogRepository.
/// Implements the <see cref="Sample.Repository.Interface.IBlogRepository" />
/// </summary>
/// <seealso cref="Sample.Repository.Interface.IBlogRepository" />
public class BlogRepository : IBlogRepository
{
/// <summary>
/// The database
/// </summary>
private readonly BloggingContext _context;

/// <summary>
/// Initializes a new instance of the <see cref="BlogRepository"/> class.
/// </summary>
/// <param name="context">The context.</param>
public BlogRepository(BloggingContext context)
{
this._context = context;
}

/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blog">實體</param>
/// <returns></returns>
public async Task<bool> AddAsync(Blog blog)
{
_context.Add(blog);
var count = await _context.SaveChangesAsync();

return count > 0;
}

/// <summary>
/// 取得 Blog
/// </summary>
/// <param name="condition">查詢條件</param>
/// <returns></returns>
public async Task<IEnumerable<Blog>> GetAsync(BlogQuery condition)
{
var blogs = await _context.Blog.ToListAsync();
return blogs;
}

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="id">blog id</param>
/// <returns></returns>
public async Task<bool> RemoveAsync(int id)
{
var blog = await _context.Blog.FindAsync(id);
_context.Blog.Remove(blog);
var count = await _context.SaveChangesAsync();

return count > 0;
}

/// <summary>
/// 更新 Blog
/// </summary>
/// <param name="blog">實體</param>
/// <returns></returns>
public async Task<bool> UpdateAsync(Blog blog)
{
_context.Update(blog);
var count = await _context.SaveChangesAsync();

return count > 0;
}
}

EFCore 的注入寫法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
var connection = this.Configuration.GetConnectionString("DefaultConnection");

// DI Register
services.AddScoped<IBlogService, BlogService>();
services.AddScoped<IBlogRepository, BlogRepository>();

services.AddDbContext<BloggingContext>(
options => options.UseSqlServer(connection));
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

///...............
}

專用型 Repository with EFCore 程式碼範例

泛型 Repository 模式 with EFCore

若按照上面的定義,EF 定義了每個 Entity 就是一個資料表,所以要搭配 Repository 模式 應該就是適合泛型 IRepository 囉 ?

答案是:不盡然,目前兩派爭論不休。

一派說法是: EF 的 Entity 其實完整實作了 CRUD 功能,如此再包一層 IRepository 定義的 CURD 是多此一舉?

而另一派說法則是:為了要隔離 EF 多一層 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
35
36
/// <summary>
/// Interface Repository
/// </summary>
public interface IGenericRepository<TEntity> where TEntity : class
{
/// <summary>
/// 新增
/// </summary>
/// <param name="entity">實體</param>
void Add(TEntity entity);

/// <summary>
/// 取得全部
/// </summary>
/// <returns></returns>
Task<ICollection<TEntity>> GetAllAsync();

/// <summary>
/// 取得單筆
/// </summary>
/// <param name="predicate">查詢條件</param>
/// <returns></returns>
Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate);

/// <summary>
/// 刪除
/// </summary>
/// <param name="entity">實體</param>
void Remove(TEntity entity);

/// <summary>
/// 更新
/// </summary>
/// <param name="entity">實體</param>
void Update(TEntity entity);
}

泛型 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <seealso cref="Sample.Repository.Interface.IGenericRepository{TEntity}" />
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
/// <summary>
/// UnitOfWork 實體
/// </summary>
private readonly IUnitOfWork _unitOfWork;

/// <summary>
/// Initializes a new instance of the <see cref="GenericRepository{TEntity}"/> class.
/// </summary>
/// <param name="unitofwork">The unitofwork.</param>
public GenericRepository(IUnitOfWork unitofwork)
{
this._unitOfWork = unitofwork;
}

/// <summary>
/// 新增
/// </summary>
/// <param name="entity">實體</param>
public void Add(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

_unitOfWork.Context.Set<TEntity>().Add(entity);
}

/// <summary>
/// 取得全部
/// </summary>
/// <returns></returns>
public async Task<ICollection<TEntity>> GetAllAsync()
{
return await this._unitOfWork.Context.Set<TEntity>().ToListAsync();
}

/// <summary>
/// 取得單筆
/// </summary>
/// <param name="predicate">查詢條件</param>
/// <returns></returns>
public async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate)
{
return await this._unitOfWork.Context.Set<TEntity>().FirstOrDefaultAsync(predicate);
}

/// <summary>
/// 刪除
/// </summary>
/// <param name="entity">實體</param>
public void Remove(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

this._unitOfWork.Context.Entry(entity).State = EntityState.Deleted;
}

/// <summary>
/// 更新
/// </summary>
/// <param name="entity">實體</param>
public void Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

this._unitOfWork.Context.Entry(entity).State = EntityState.Modified;
}
}

注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
var connection = this.Configuration.GetConnectionString("DefaultConnection");

// DI Register
services.AddScoped<IBlogService, BlogService>();
services.AddScoped<IGenericRepository<Blog>, GenericRepository<Blog>>();
services.AddScoped<DbContext, BloggingContext>();
services.AddDbContext<BloggingContext>(
options => options.UseSqlServer(connection));

泛型 Repository wit EF Core 範例

此範例為簡單情境,只有一個 BlogRepositoy,故在 Startup.cs 作簡單註冊,若是有多個 Repository 可以用以下兩種方式

方法1 : Register with key

DI 設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
services.AddScoped<ServiceA>();
services.AddScoped<ServiceB>();
services.AddScoped<ServiceC>();

services.AddScoped<ServiceResolver>(serviceProvider => key =>
{
switch (key)
{
case "A":
return serviceProvider.GetService<ServiceA>();
case "B":
return serviceProvider.GetService<ServiceB>();
case "C":
return serviceProvider.GetService<ServiceC>();
default:
throw new KeyNotFoundException(); // or maybe return null, up to you
}
});

產生實體

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Consumer
{
private readonly IService _aService;

public Consumer(ServiceResolver serviceAccessor)
{
_aService = serviceAccessor("A");
}

public void UseServiceA()
{
_aService.DoTheThing();
}
}
方法2 : 實作多個 Repository,各別註冊
1
2
services.AddScoped<IBlogRepository,BlogRepository();
services.AddScoped<IPostRepository,PostRepository();

BlogRepository, PostRepository 實作 IGenericRepository。

Repository Pattern & Unit Of Work (常見用法版)

一步步說明 UnitOfWork IRepository 與 Service 的問題

若事情只到這也就還好,但當 UnitOfWork 也進來之後,事情變得更複雜,主要原因是一般提供的 UnitOfWork 的作法有其問題,我們來一步步說明。

首先,關於IUnitOfWork 的定義實現

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/// <summary>
/// UnitOfWork 介面
/// </summary>
/// <seealso cref="System.IDisposable" />
public interface IUnitOfWork : IDisposable
{
/// <summary>
/// DB Context
/// </summary>
DbContext Context { get; }

/// <summary>
/// Saves the change.
/// </summary>
/// <returns></returns>
Task<int> SaveChangeAsync();
}

/// <summary>
/// UnitOfWork
/// </summary>
public class UnitOfWork : IUnitOfWork
{
/// <summary>
///
/// </summary>
private bool disposed = false;

/// <summary>
/// 建構式
/// </summary>
/// <param name="context"></param>
public UnitOfWork(DbContext context)
{
this.Context = context;
}

/// <summary>
/// Context
/// </summary>
public DbContext Context { get; private set; }

/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// SaveChange
/// </summary>
/// <returns></returns>
public async Task<int> SaveChangeAsync()
{
return await this.Context.SaveChangesAsync();
}

/// <summary>
/// Dispose
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
this.Context.Dispose();
this.Context = null;
}
}
this.disposed = true;
}
}

接下來進行 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <seealso cref="Sample.Repository.Interface.IGenericRepository{TEntity}" />
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
/// <summary>
/// UnitOfWork 實體
/// </summary>
private readonly IUnitOfWork _unitOfWork;

/// <summary>
/// Initializes a new instance of the <see cref="GenericRepository{TEntity}"/> class.
/// </summary>
/// <param name="unitofwork">The unitofwork.</param>
public GenericRepository(IUnitOfWork unitofwork)
{
this._unitOfWork = unitofwork;
}

/// <summary>
/// 新增
/// </summary>
/// <param name="entity">實體</param>
/// <exception cref="ArgumentNullException">entity</exception>
public void Add(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

_unitOfWork.Context.Set<TEntity>().Add(entity);
}

/// <summary>
/// 取得全部
/// </summary>
/// <returns></returns>
public async Task<ICollection<TEntity>> GetAllAsync()
{
return await this._unitOfWork.Context.Set<TEntity>().ToListAsync();
}

/// <summary>
/// 取得單筆
/// </summary>
/// <param name="predicate">查詢條件</param>
/// <returns></returns>
public async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate)
{
return await this._unitOfWork.Context.Set<TEntity>().FirstOrDefaultAsync(predicate);
}

/// <summary>
/// 刪除
/// </summary>
/// <param name="entity">實體</param>
/// <exception cref="ArgumentNullException">entity</exception>
public void Remove(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

this._unitOfWork.Context.Entry(entity).State = EntityState.Deleted;
}

/// <summary>
/// 更新
/// </summary>
/// <param name="entity">實體</param>
/// <exception cref="ArgumentNullException">entity</exception>
public void Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}

this._unitOfWork.Context.Entry(entity).State = EntityState.Modified;
}
}

再來是 Service 的修改

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/// <summary>
/// BlogService
/// </summary>
public class BlogService : IBlogService
{
private IGenericRepository<Blog> _blogRepository;
private IMapper _mapper;
private IUnitOfWork _unitofwork;

/// <summary>
/// Initializes a new instance of the <see cref="BlogService"/> class.
/// </summary>
public BlogService(
IUnitOfWork unitofwork,
IGenericRepository<Blog> blogRepository,
IMapper mapper)
{
this._unitofwork = unitofwork;
this._blogRepository = blogRepository;
this._mapper = mapper;
}

/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> AddAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

this._blogRepository.Add(blog);
return await this._unitofwork.SaveChangeAsync();
}

/// <summary>
/// 取得 Blog
/// </summary>
/// <param name="blogQueryDto">查詢條件</param>
/// <returns></returns>
public async Task<IEnumerable<BlogDto>> GetAsync(BlogQueryDto blogQueryDto)
{
var blogs = await this._blogRepository.GetAllAsync();

// Convert Blog to BlogDto
var blogDtos = this._mapper.Map<IEnumerable<BlogDto>>(blogs);

return blogDtos;
}

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="blogId">Blog Id</param>
/// <returns></returns>
public async Task<int> RemoveAsync(int blogId)
{
var blog = await this._blogRepository.GetAsync(x => x.BlogId == blogId);

this._blogRepository.Remove(blog);
return await this._unitofwork.SaveChangeAsync();
}

/// <summary>
/// 修改 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> UpdateAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

this._blogRepository.Update(blog);
return await this._unitofwork.SaveChangeAsync();
}
}

Repository Pattern & Unit Of Work (常見用法版) 程式碼範例

問題來了?在StudentService 中,StudentRepository 似乎變得有些多餘,因為它所做的,UnitOfWork 也都可以做,隨著項目的複雜,這樣就會造成很多的問題,比如:

  • IUnitOfWork 的職責不明確。
  • Repository 的職責不明確。
  • Service 很困惑,因為它不知道該使用誰。
  • Service 的代碼越來越亂。

Repository Pattern & Unit Of Work (常見用法版)

較好的使用方式 - 職責分離

首先先調整 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    /// <summary>
/// UnitOfWork 介面
/// </summary>
/// <seealso cref="System.IDisposable" />
public interface IUnitOfWork : IDisposable
{
/// <summary>
///
/// </summary>
IGenericRepository<Blog> BlogRepository { get; }

/// <summary>
/// DB Context
/// </summary>
DbContext Context { get; }

/// <summary>
/// Saves the change.
/// </summary>
/// <returns></returns>
Task<int> SaveChangeAsync();
}
}

/// <summary>
/// UnitOfWork
/// </summary>
public class UnitOfWork : IUnitOfWork
{
/// <summary>
///
/// </summary>
private bool disposed = false;

/// <summary>
/// Initializes a new instance of the <see cref="UnitOfWork"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="blogRepository">The blog repository.</param>
public UnitOfWork(
DbContext context,
IGenericRepository<Blog> blogRepository)
{
this.Context = context;
this.BlogRepository = blogRepository;
}

/// <summary>
/// </summary>
public IGenericRepository<Blog> BlogRepository { get; private set; }

/// <summary>
/// Context
/// </summary>
public DbContext Context { get; private set; }

/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// SaveChange
/// </summary>
/// <returns></returns>
public async Task<int> SaveChangeAsync()
{
return await this.Context.SaveChangesAsync();
}

/// <summary>
/// Dispose
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
this.Context.Dispose();
this.Context = null;
}
}
this.disposed = true;
}
}

再來修改 Repository 的實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <seealso cref="Sample.Repository.Implement.GenericRepository{TEntity}" />
public class BlogRepository<TEntity> : GenericRepository<TEntity> where TEntity : class
{
/// <summary>
/// Initializes a new instance of the <see cref="BlogRepository{TEntity}"/> class.
/// </summary>
/// <param name="context">db context.</param>
public BlogRepository(DbContext context) : base(context)
{
}
}

再來是 Service 的修改

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/// <summary>
/// BlogService
/// </summary>
public class BlogService : IBlogService
{
private IMapper _mapper;
private IUnitOfWork _unitofwork;

/// <summary>
/// Initializes a new instance of the <see cref="BlogService"/> class.
/// </summary>
public BlogService(
IUnitOfWork unitofwork,
IMapper mapper)
{
this._unitofwork = unitofwork;
this._mapper = mapper;
}

/// <summary>
/// 新增 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> AddAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

this._unitofwork.BlogRepository.Add(blog);
return await this._unitofwork.SaveChangeAsync();
}

/// <summary>
/// 取得 Blog
/// </summary>
/// <param name="blogQueryDto">查詢條件</param>
/// <returns></returns>
public async Task<IEnumerable<BlogDto>> GetAsync(BlogQueryDto blogQueryDto)
{
var blogs = await this._unitofwork.BlogRepository.GetAllAsync();

// Convert Blog to BlogDto
var blogDtos = this._mapper.Map<IEnumerable<BlogDto>>(blogs);

return blogDtos;
}

/// <summary>
/// 刪除 Blog
/// </summary>
/// <param name="blogId">Blog Id</param>
/// <returns></returns>
public async Task<int> RemoveAsync(int blogId)
{
var blog = await this._unitofwork.BlogRepository.GetAsync(x => x.BlogId == blogId);

this._unitofwork.BlogRepository.Remove(blog);
return await this._unitofwork.SaveChangeAsync();
}

/// <summary>
/// 修改 Blog
/// </summary>
/// <param name="blogDto">Blog Dto</param>
/// <returns></returns>
public async Task<int> UpdateAsync(BlogDto blogDto)
{
// Convert BlogDto to Blog
var blog = this._mapper.Map<Blog>(blogDto);

this._unitofwork.BlogRepository.Update(blog);
return await this._unitofwork.SaveChangeAsync();
}
}

如此,是不是就明確許多。

整個專案的架構圖如下

Repository Pattern & Unit Of Work (職責分離版)

Repository Pattern & Unit Of Work (職責分離版) 程式碼範例

總結

本篇完整介紹了 Repoistory 與 UnitOfWork 的應用範例,希望能幫助到大家。