[鐵人賽 Day24] ASP.NET Core 2 系列 - Entity Framework Core

-- Pageviews

Entity Framework 是 .NET 跟資料庫溝通好用的 Object-Relational Mapper (O/RM) 框架,ASP.NET Core 也在專案初期就加入了 Entity Framework Core (EF Core),延續這個好用框架。
本篇將介紹 ASP.NET Core 搭配 Entity Framework Core 存取 SQL Server 資料庫,是以 Code First 方式建立資料表。

iT 邦幫忙 2018 鐵人賽 - Modern Web 組參賽文章:
[Day24] ASP.NET Core 2 系列 - Entity Framework Core

安裝套件

要在 ASP.NET Core 中使用 Entity Framework Core,需要安裝 Microsoft.EntityFrameworkCore 套件。
透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.EntityFrameworkCore

Entity Framework 基本上都是搭配 SQL Server,以下範例也是使用 SQL Server。
如果沒有裝 SQL Server 可以從官網下載安裝,Linux/macOS 有 Docker 版本可以使用。
Download SQL Server 2017

SQL Server 2017 超級佛心!!!
過去 Express edition 就已經是免費版本,這次依然免費,不怎麼意外。
但這次連 Developer edition 都變成免費

建立 DbContext

DbContext 是 EF Core 跟資料庫溝通的主要類別,透過繼承 DbContext 可以定義跟資料庫溝通的行為。
首先我們先建立一個類別繼承 DbContext,同時建立 DbSet。

MyContext.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using Microsoft.EntityFrameworkCore;
using MyWebsite.Models;

namespace MyWebsite
{
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
}

public DbSet<UserModel> Users { get; set; }
}
}

EF Core 會幫我們把 DbSet 轉換成資料表。

建立一個資料模型 UserModel,這個資料模型會被轉成資料表,把屬性 Id 設定成 Primary Key 且自動遞增:

Models\UserModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MyWebsite.Models
{
public class UserModel
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }

[Required]
public string Name { get; set; }

public string Email { get; set; }

public string PhoneNumber { get; set; }

public string Address { get; set; }
}
}

設定資料庫

新增一個 *.json 檔案來設定資料庫的連線字串。
settings.json

1
2
3
4
5
{
"ConnectionStrings": {
"DefaultConnection": "Server=(LocalDB)\MSSQLLocalDB;Database=MyWebsite;"
}
}

(LocalDB)\MSSQLLocalDB 是 SQL Server 提供的本機 DB。

在 WebHost Builder 用 ConfigureAppConfiguration 載入組態設定:

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((webHostBuilder, configurationBinder) =>
{
configurationBinder.AddJsonFile("settings.json", optional: true);
})
.UseStartup<Startup>()
.Build();
}

Startup.ConfigureServices 注入 EF Core 的服務 DbContext,並設定資料庫連線字串。
並在 Startup.Configure 呼叫 dbContext.Database.EnsureCreated(),當啟動 Website 時就會建立資料庫。

Startup.cs

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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
private readonly IConfiguration _config;

public Startup(IConfiguration config)
{
_config = config;
}

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<MyContext>(options =>
{
options.UseSqlServer(_config.GetConnectionString("DefaultConnection"));
});
}

public void Configure(IApplicationBuilder app, MyContext dbContext)
{
// 建立資料庫
dbContext.Database.EnsureCreated();
app.UseMvcWithDefaultRoute();
}
}
}

CRUD

因為在 DI 容器註冊了 MyContext,所以在 Controller 的建構子就透過 DI 可以取得 MyContext 實例。
透過 MyContext 就可以把物件資料以集合的形式在資料庫輕鬆存取。

Controllers\UserController.cs

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
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;

namespace MyWebsite.Controllers
{
[Route("api/[controller]s")]
public class UserController : Controller
{
private readonly MyContext _context;

public UserController(MyContext context)
{
_context = context;
}

[HttpGet]
public ResultModel Get(string q)
{
var result = new ResultModel();
result.Data = _context.Users.Where(x => string.IsNullOrEmpty(q)
|| Regex.IsMatch(x.Name, q, RegexOptions.IgnoreCase));
result.IsSuccess = true;
return result;
}

[HttpGet("{id}")]
public ResultModel Get(int id)
{
var result = new ResultModel();
result.Data = _context.Users.SingleOrDefault(x => x.Id == id);
result.IsSuccess = true;
return result;
}

[HttpPost]
public ResultModel Post([FromBody]UserModel user)
{
var result = new ResultModel();
_context.Users.Add(user);
_context.SaveChanges();
result.Data = user.Id;
result.IsSuccess = true;
return result;
}

[HttpPut("{id}")]
public ResultModel Put([FromBody]UserModel user)
{
var result = new ResultModel();
var oriUser = _context.Users.SingleOrDefault(x => x.Id == user.Id);
if (oriUser != null)
{
_context.Entry(oriUser).CurrentValues.SetValues(user);
_context.SaveChanges();
result.IsSuccess = true;
}
return result;
}

[HttpDelete("{id}")]
public ResultModel Delete(int id)
{
var result = new ResultModel();
var oriUser = _context.Users.SingleOrDefault(x => x.Id == id);
if (oriUser != null)
{
_context.Users.Remove(oriUser);
_context.SaveChanges();
result.IsSuccess = true;
}
return result;
}
}
}

仔細看一下 _context,會發現我都沒有對它做 Dispose。
不是 EF Core 改成自動 Dispose,而是 AddDbContext 服務幫我們實做了 Dispose。
當 Request 進來時,MyContext 會開啟連線;Response 結束時,會關閉連線並呼叫 Dispose。

Repository Pattern

由於 Entity Framework 跟邏輯的相依性太強,對單元測試很不友善,所以通常都會搭配 Repository Pattern 使用。
Repository Pattern 切斷相依的介面如下:

Repositories\IRepository.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace MyWebsite.Repositories
{
public interface IRepository<TEntity, TKey>
where TEntity : class
{
TKey Create(TEntity entity);

void Update(TEntity entity);

void Delete(TKey id);

TEntity FindById(TKey id);

IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> expression);
}
}

把存取 MyContext.Users 的邏輯都實作在 UserRepository。

Repositories\UserRepository.cs

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using MyWebsite.Models;

namespace MyWebsite.Repositories
{
public class UserRepository : IRepository<UserModel, int>
{
private readonly MyContext _context;

public UserRepository(MyContext context)
{
_context = context;
}

public int Create(UserModel entity)
{
_context.Users.Add(entity);
_context.SaveChanges();
return entity.Id;
}

public void Update(UserModel entity)
{
var oriUser = _context.Users.Single(x => x.Id == entity.Id);
_context.Entry(oriUser).CurrentValues.SetValues(entity);
_context.SaveChanges();
}

public void Delete(int id)
{
_context.Users.Remove(_context.Users.Single(x => x.Id == id));
_context.SaveChanges();
}

public IEnumerable<UserModel> Find(Expression<Func<UserModel, bool>> expression)
{
return _context.Users.Where(expression);
}

public UserModel FindById(int id)
{
return _context.Users.SingleOrDefault(x => x.Id == id);
}
}
}

Startup.ConfigureServices 注入 UserRepository

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...
namespace MyWebsite
{
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<MyContext>(options =>
{
options.UseSqlServer(_config.GetConnectionString("DefaultConnection"));
});
services.AddScoped<IRepository<UserModel, int>, UserRepository>();
}
// ...
}
}

若不了解 AddScoped 請參考這篇:[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入 (Dependency Injection)

原本在 UserController 注入 MyContext 改成注入 IRepository<UserModel, int>

Controllers\UserController.cs

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
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;
using MyWebsite.Repositories;

namespace MyWebsite.Controllers
{
[Route("api/[controller]s")]
public class UserController : Controller
{
private readonly IRepository<UserModel, int> _repository;

public UserController(IRepository<UserModel, int> repository)
{
_repository = repository;
}

[HttpGet]
public ResultModel Get(string q)
{
var result = new ResultModel();
result.Data = _repository.Find(x => string.IsNullOrEmpty(q)
|| Regex.IsMatch(x.Name, q, RegexOptions.IgnoreCase));
result.IsSuccess = true;
return result;
}

[HttpGet("{id}")]
public ResultModel Get(int id)
{
var result = new ResultModel();
result.Data = _repository.FindById(id);
result.IsSuccess = true;
return result;
}

[HttpPost]
public ResultModel Post([FromBody]UserModel user)
{
var result = new ResultModel();
_repository.Create(user);
result.Data = user.Id;
result.IsSuccess = true;
return result;
}

[HttpPut("{id}")]
public ResultModel Put(int id, [FromBody]UserModel user)
{
var result = new ResultModel();
try
{
user.Id = id;
_repository.Update(user);
result.IsSuccess = true;
}
catch
{
// ...
}
return result;
}

[HttpDelete("{id}")]
public ResultModel Delete(int id)
{
var result = new ResultModel();
try
{
_repository.Delete(id);
result.IsSuccess = true;
}
catch
{
// ...
}
return result;
}
}
}

執行結果

[鐵人賽 Day24] ASP.NET Core 2 系列 - Entity Framework Core

參考

Getting started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
Create, Read, Update, and Delete - EF Core with ASP.NET Core MVC tutorial