EF Core 簡介
Entity Framework Core 是微軟自家的 ORM 框架。作為 .Net Core 生態中的一個重要組成部分,它是一個支持跨平台的全新版本,用三個詞來概況 EF Core 的特點:輕量級、可擴展、跨平台。
目前 EF Core 支持的數據庫:
- Microsoft SQL Server
- SQLite
- Postgres (Npgsql)
- SQL Server Compact Edition
- InMemory (for testing purposes)
- MySQL
- IBM DB2
- Oracle
- Firebird
使用 EF Core(Code First)
-
新建一個 WebAPI 項目
-
通過 Nuget 安裝 EF Core 引用
// SQL Server Install-Package Microsoft.EntityFrameworkCore.SqlServer
其他數據庫請查看:https://docs.microsoft.com/zh-cn/ef/core/providers/
-
添加實體
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
-
添加數據庫上下文
public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
有兩種方式配置數據庫連接,一種是注冊 Context 的時候提供 options。比較推薦這種方式。
public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
在 Startup 中配置
public void ConfigureServices(IServiceCollection services) { var connectionString = @"Server=.;Database=Blogging;Trusted_Connection=True;"; services.AddDbContext<BloggingContext>(o => o.UseSqlServer(connectionString)); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
一種是重載 OnConfiguring 方法提供連接字符串:
public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=.;Database=Blogging;Trusted_Connection=True;"); base.OnConfiguring(optionsBuilder); } }
-
在Controller 中使用 Context
public class BlogsController : ControllerBase { private readonly BloggingContext _context; public BlogsController(BloggingContext context) { _context = context; } // GET: api/Blogs [HttpGet] public IEnumerable<Blog> GetBlogs() { return _context.Blogs; } }
遷移 Migration
-
通過 Nuget 引入EF Core Tool 的引用
Install-Package Microsoft.EntityFrameworkCore.Tools
如果需要使用
dotnet ef
命令, 請添加 Microsoft.EntityFrameworkCore.Tools.DotNet -
生成遷移
打開Package Manager Console,執行命令
Add-Migration InitialCreate
。
執行成功后會在項目下生成一個 Migrations目錄,包含兩個文件:- BloggingContextModelSnapshot:當前Model的快照(狀態)。
- 20180828074905_InitialCreate:這里面包含着migration builder需要的代碼,用來遷移這個版本的數據庫。里面有Up方法,就是從當前版本升級到下一個版本;還有Down方法,就是從下一個版本再退回到當前版本。
-
更新遷移到數據庫
執行命令
Update-Database
。
如果執行成功,數據庫應該已經創建成功了。現在可以測試剛才創建的WebAPI應用了。使用代碼
Database.Migrate();
可以達到同樣的目的public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { Database.Migrate(); }
EF Core 中的一些常用知識點
實體建模
EF 根據對 Model 的配置生成表和字段,主要有三種配置方式:
-
約定 根據約定(Id 或者
Id)會被視為映射表的主鍵,並且該主鍵是自增的。 -
Data Annotation 數據注解
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; public class Blog { [Key] [Column("BlogId")] public int BlogId { get; set; } [Required] [MaxLength(500)] public string Url { get; set; } public int Rating { get; set; } public List<Post> Posts { get; set; } }
- Key: 主鍵
- Required:不能為空
- MinLength:字符串最小長度
- MaxLength:字符串最大長度
- StringLength:字符串最大長度
- Timestamp:rowversion,時間戳列
- ConcurrencyCheck 樂觀並發檢查列
- Table 表名
- Column 字段名
- Index 索引
- ForeignKey 外鍵
- NotMapped 不映射數據庫中的任何列
- InverseProperty 指定導航屬性和實體關系的對應,用於實體中有多個關系映射。
-
Fluent API
通過 Fluent API 在 IEntityTypeConfiguration 實現類里面配置實體:
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List<Post> Posts { get; set; } } public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { builder.HasKey(t => t.BlogId); builder.Property(t => t.Url).IsRequired().HasMaxLength(500); } }
並在 Context 的 OnModelCreating 方法里面應用:
public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) {} public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new BlogConfiguration()); } }
Fluent API 比數據注解有更高的優先級。
實體關系
-
一對多關系
Blog 和 Post 是一對多關系,在 PostConfiguration 里面添加如下配置:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } public class PostConfiguration : IEntityTypeConfiguration<Post> { public void Configure(EntityTypeBuilder<Post> builder) { builder.HasOne<Blog>(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey(p => p.BlogId) .OnDelete(DeleteBehavior.Cascade); } }
-
一對一關系
創建一個實體類 PostExtension 做為 Post 的擴展表,它們之間是一對一關系。
如果兩個實體相互包括了對方的引用導航屬性(本例中是PostExtension Extension
和Post Post
)和外鍵屬性 (本例中是 PostExtension 中的PostId
),那 EF Core 會默認配置一對一關系的,當然也可以手動寫語句(如注釋的部分)。public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public PostExtension Extension { get; set; } } public class PostExtension { public int PostId { get; set; } public string ExtensionField1 { get; set; } public Post Post { get; set; } } public class PostExtensionConfiguration : IEntityTypeConfiguration<PostExtension> { public PostExtensionConfiguration() { } public void Configure(EntityTypeBuilder<PostExtension> builder) { builder.HasKey(t => t.PostId); //builder.HasOne(e => e.Post) // .WithOne(p => p.Extension) // .HasForeignKey<PostExtension>(e => e.PostId) // .OnDelete(DeleteBehavior.Cascade); } }
-
多對多關系
創建一個實體類 Tag, 和 Blog 是多對多關系。一個 Blog 可以有多個不同 Tag,同時一個 Tag 可以用多個 Blog。
EF Core 中創建多對多關系必須要聲明一個映射的關系實體,所以我們創建 BlogTag 實體,並在 BlogTagConfiguration 配置了多對多關系。public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public IList<BlogTag> BlogTags { get; set; } } public class Tag { public int TagId { get; set; } public string TagName { get; set; } public IList<BlogTag> BlogTags { get; set; } } public class BlogTag { public int BlogId { get; set; } public Blog Blog { get; set; } public int TagId { get; set; } public Tag Tag { get; set; } } public class BlogTagConfiguration : IEntityTypeConfiguration<BlogTag> { public void Configure(EntityTypeBuilder<BlogTag> builder) { builder.HasKey(bt => new { bt.BlogId, bt.TagId }); builder.HasOne<Blog>(bt => bt.Blog) .WithMany(b => b.BlogTags) .HasForeignKey(bt => bt.BlogId); builder.HasOne<Tag>(bt => bt.Tag) .WithMany(t => t.BlogTags) .HasForeignKey(bt => bt.TagId); } }
種子數據
填充種子數據可以讓我們在首次使用應用之前向數據庫中插入一些初始化數據。有兩種方法:
-
通過實體類配置實現
在配置實體的時候可以通過HasData
方法預置數據,在執行Update-Database
命令時候會寫入數據庫。public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { //Data Seeding builder.HasData(new Blog { BlogId = 1, Url = "http://sample.com/1", Rating = 0 }); } }
-
統一配置
創建一個統一配置 SeedData 類, 然后在 Program.cs 中的 Main 中調用它。public static class SeedData { public static void Initialize(IServiceProvider serviceProvider) { using (var context = new BloggingContext( serviceProvider.GetRequiredService<DbContextOptions<BloggingContext>>())) { if (context.Blogs.Any()) return; // DB has been seeded var blogs = new List<Blog> { new Blog { Url = "http://sample.com/2", Rating = 0 }, new Blog { Url = "http://sample.com/3", Rating = 0 }, new Blog { Url = "http://sample.com/4", Rating = 0 } }; context.Blogs.AddRange(blogs); context.SaveChanges(); } } }
public class Program { public static void Main(string[] args) { //CreateWebHostBuilder(args).Build().Run(); var host = CreateWebHostBuilder(args).Build(); using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; try { SeedData.Initialize(services); } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred seeding the DB."); } } host.Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
並發管理
數據庫並發指的是多個進程或用戶同時訪問或更改數據庫中的相同數據的情況。 並發控制指的是用於在發生並發更改時確保數據一致性的特定機制。
- 樂觀並發:無論何時從數據庫請求數據,數據都會被讀取並保存到應用內存中。數據庫級別沒有放置任何顯式鎖。數據操作會按照數據層接收到的順序執行。
- 悲觀並發:無論何時從數據庫請求數據,數據都會被讀取,然后該數據上就會加鎖,因此沒有人能訪問該數據。這會降低並發相關問題的機會,缺點是加鎖是一個昂貴的操作,會降低整個應用程序的性能。
EF Core 默認支持樂觀並發控制,這意味着它將允許多個進程或用戶獨立進行更改而不產生同步或鎖定的開銷。 在理想情況下,這些更改將不會相互影響,因此能夠成功。 在最壞的情況下,兩個或更多進程將嘗試進行沖突更改,其中只有一個進程應該成功。
-
ConcurrencyCheck
/IsConcurrencyToken
ConcurrencyCheck 特性可以應用到領域類的屬性中。當EF執行更新或刪除操作時,EF Core 會將配置的列放在 where 條件語句中。執行這些語句后,EF Core 會讀取受影響的行數。如果未影響任何行,將檢測到並發沖突引發 DbUpdateConcurrencyException。public class Blog { public int BlogId { get; set; } public string Url { get; set; } [ConcurrencyCheck] public int Rating { get; set; } }
[HttpPut("{id}")] public async Task<IActionResult> PutBlog([FromRoute] int id, [FromBody] Blog blog) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var dbModel = await _context.Blogs.FindAsync(id); dbModel.Url = blog.Url; dbModel.Rating = blog.Rating; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { //todo: handle DbUpdateConcurrencyException throw ex; } return NoContent(); }
通過 SQL Server Profiler 查看生成的 SQL Update 語句。
exec sp_executesql N'SET NOCOUNT ON; UPDATE [Blogs] SET [Rating] = @p0, [Url] = @p1 WHERE [BlogId] = @p2 AND [Rating] = @p3; SELECT @@ROWCOUNT; ',N'@p2 int,@p0 int,@p3 int,@p1 nvarchar(500)',@p2=1,@p0=999,@p3=20,@p1=N'http://sample.com/1'
-
Timestamp
/IsRowVersion
TimeStamp特性可以應用到領域類中,只有一個字節數組的屬性上面。每次插入或更新行時,由數據庫生成一個新的值做為並發標記。public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } [Timestamp] public byte[] Timestamp { get; set; } }
通過 SQL Server Profiler 查看生成的 SQL Update 語句。
exec sp_executesql N'SET NOCOUNT ON; UPDATE [Blogs] SET [Rating] = @p0 WHERE [BlogId] = @p1 AND [Timestamp] = @p2; SELECT [Timestamp] FROM [Blogs] WHERE @@ROWCOUNT = 1 AND [BlogId] = @p1; ',N'@p1 int,@p0 int,@p2 varbinary(8)',@p1=1,@p0=8888,@p2=0x00000000000007D1
處理沖突的策略:
- 忽略沖突並強制更新:這種策略是讓所有的用戶更改相同的數據集,然后所有的修改都會經過數據庫,這就意味着數據庫會顯示最后一次更新的值。這種策略會導致潛在的數據丟失,因為許多用戶的更改都丟失了,只有最后一個用戶的更改是可見的。
- 部分更新:在這種情況中,我們也允許所有的更改,但是不會更新完整的行,只有特定用戶擁有的列更新了。這就意味着,如果兩個用戶更新相同的記錄但卻不同的列,那么這兩個更新都會成功,而且來自這兩個用戶的更改都是可見的。
- 拒絕更改:當一個用戶嘗試更新一個記錄時,但是該記錄自從他讀取之后已經被別人修改了,此時告訴該用戶不允許更新該數據,因為數據已經被某人更新了。
- 警告詢問用戶:當一個用戶嘗試更新一個記錄時,但是該記錄自從他讀取之后已經被別人修改了,這時應用程序就會警告該用戶該數據已經被某人更改了,然后詢問他是否仍然要重寫該數據還是首先檢查已經更新的數據。
執行 SQL 語句和存儲過程
EF Core 使用以下方法執行 SQL 語句和存儲過程:
-
DbSet
.FromSql() DbSet<TEntity>.FromSql()
返回值為IQueryable,可以與Linq擴展方法配合使用。注意:- SQL 查詢必須返回實體或查詢類型的所有屬性的數據
- 結果集中的列名必須與屬性映射到的列名稱匹配。
- SQL 查詢不能包含相關數據。 但是可以使用 Include 運算符返回相關數據。
- 不要使用 TOP 100 PERCENT 或 ORDER BY 等子句。可以通過 Linq 在代碼里面編寫。
基本 SQL 查詢
var blogs = _context.Blogs.FromSql($"select * from Blogs").ToList();
帶有參數的查詢:
var blog = _context.Blogs.FromSql($"select * from Blogs where BlogId = {id}");
使用 LINQ:
var blogs = _context.Blogs.FromSql($"select * from Blogs") .OrderByDescending(r => r.Rating) .Take(2) .ToList();
通過 SQL Server Profiler 查看 SQL 語句,可以發現 EF Core 是把手工寫的 SQL 語句和 Linq 合並生成了一條語句:
exec sp_executesql N'SELECT TOP(@__p_1) [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url] FROM ( select * from Blogs ) AS [r] ORDER BY [r].[Rating] DESC',N'@__p_1 int',@__p_1=2
使用 Include 包括相關數據
var blogs = _context.Blogs.FromSql($"select * from Blogs").Include(r => r.Posts).ToList();
通過 SQL Server Profiler 查看 SQL 語句:
SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url] FROM ( select * from Blogs ) AS [b] ORDER BY [b].[BlogId] SELECT [b.Posts].[PostId], [b.Posts].[BlogId], [b.Posts].[Content], [b.Posts].[Title] FROM [Posts] AS [b.Posts] INNER JOIN ( SELECT [b0].[BlogId] FROM ( select * from Blogs ) AS [b0] ) AS [t] ON [b.Posts].[BlogId] = [t].[BlogId] ORDER BY [t].[BlogId]
-
DbContext.Database.ExecuteSqlCommand()
ExecuteSqlCommand
方法返回一個整數,表示執行的SQL語句影響的行數。有效的操作是 INSERT、UPDATE 和 DELETE,不能用於返回實體。測試一下 INSERT:
int affectRows = _context.Database.ExecuteSqlCommand($"Insert into Blogs([Url],[Rating])Values({blog.Url}, {blog.Rating})");
通過 SQL Server Profiler 查看 SQL 語句:
exec sp_executesql N'Insert into Blogs([Url],[Rating])Values(@p0, @p1)',N'@p0 nvarchar(4000),@p1 int',@p0=N'testurl',@p1=3
延遲加載和預先加載
EF Core 通過在模型中使用導航屬性來加載相關實體。 有三種常見模式可用於加載相關數據。
-
預先加載
表示從數據庫中加載相關數據,作為初始查詢的一部分。使用Include
方法實現預加載,使用ThenInclude
實現多級預加載。var blogs = _context.Blogs.Include(r => r.Posts).ToList();
當需要 JSON 序列化 blogs 對象時候,ASP.NET Core 自帶的序列化庫 Newtonsoft.Json 可能會拋出自引用循環異常。請在 Startup 的 ConfigureServices 方法中配置以下代碼解決。
public void ConfigureServices(IServiceCollection services) { services.AddMvc() .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); }
-
顯式加載
表示稍后從數據庫中顯式加載相關數據。var blog = await _context.Blogs.FindAsync(id); _context.Entry(blog) .Collection(b => b.Posts) .Load();
-
延遲加載
表示在訪問導航屬性時,才從數據庫中加載相關數據。在 EF Core 2.1 中才引入此功能。-
Nuget 安裝 Microsoft.EntityFrameworkCore.Proxies
-
調用 UseLazyLoadingProxies 來啟用延遲加載。
services.AddDbContext<BloggingContext>(option => option.UseLazyLoadingProxies().UseSqlServer(connectionString));
-
導航屬性添加
virtual
修飾符。public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public virtual IList<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public virtual Blog Blog { get; set; } }
-
測試,當代碼執行到
var posts = blog.Posts
時候,會去數據庫里面查詢Posts記錄。var blog = await _context.Blogs.FindAsync(id); var posts = blog.Posts;
盡量避免在循環時候使用延遲加載,會導致每次循環都去訪問數據庫。
-
IQueryable 和 IEnumerable
直接通過一個實例測試一下:
var testIQueryable = _context.Blogs.Where(r => r.Rating > 10);
var testIEnumerable = _context.Blogs.AsEnumerable().Where(r => r.Rating > 10);
var testIQueryableList = testIQueryable.ToList();
var testIEnumerableList = testIEnumerable.ToList();
查看生產的 SQL 語句
-
IQueryable
SELECT [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url] FROM [Blogs] AS [r] WHERE [r].[Rating] > 10
-
IEnumerable
SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url] FROM [Blogs] AS [b]
IQueryable 是將 Linq 表達式翻譯成 T-SQL 語句之后再向 SQL 服務器發送命令.
IEnumerable 是在調用自己的 Linq 方法之前先從 SQL 服務器取到數據並加載到本地內存中。
生成遷移 SQL 腳本
EF Core 將遷移更新到生產環境可以使用 Script-Migration
命令生成sql腳本,然后到生產數據庫執行.
此命令有幾個選項。
-From <String>
遷移應是運行該腳本前應用到數據庫的最后一個遷移。 如果未應用任何遷移,請指定 0(默認值)。-To <String>
遷移是運行該腳本后應用到數據庫的最后一個遷移。 它默認為項目中的最后一個遷移。-Idempotent
此腳本僅會應用尚未應用到數據庫的遷移。 如果不確知應用到數據庫的最后一個遷移或需要部署到多個可能分別處於不同遷移的數據庫,此腳本非常有用。
待補充...
SQL 監視工具
有幾種方法可以監視 EF Core 自動生成的 SQL 語句:
-
內置日志
-
數據庫監視工具
-
Miniprofiler
-
內置日志: 在調試模式下,EF Core 會使用 ASP.NET Core 的內置日志記錄功能把生成的 SQL 語句顯示在輸出窗口,大概如下:
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (50ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url] FROM [Blogs] AS [e] WHERE [e].[Id] = @__get_Item_0
如果想查看敏感數據比如
@__get_Item_0='?'
,請在 Context 類的 OnConfiguring 方法里面配置optionsBuilder.EnableSensitiveDataLogging();
-
數據庫監視工具: 也可以通過數據庫的監視工具,比如用於監視 MS SQL 的工具 SQL Server Profiler 查看執行的 SQL 語句,大概如下:
exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url] FROM [Blogs] AS [e] WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=1
-
Miniprofiler: MiniProfiler/dotnet是一款簡單而有效的性能分析的輕量級程序,可以監控頁面,也可以監控 EF Core 執行的 SQL 語句。
MiniProfiler 一般用於 MVC 項目,但也可以結合 Swagger 用於 Web API項目。Swagger 的安裝和使用在本篇不做討論,詳細請參考Swashbuckle.AspNetCore。
-
Nuget 安裝 MiniProfiler 引用
Install-Package MiniProfiler.AspNetCore.Mvc Install-Package MiniProfiler.EntityFrameworkCore
-
修改 SwaggerUI/index.html 頁面: 在項目下面新建一個文件 SwaggerIndex.html 並復制以下代碼,設置編譯為 Embedded resource
<script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.0.138+gcc91adf599" data-version="4.0.138+gcc91adf599" data-path="/profiler/" data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa748a" data-ids="" data-position="Left" data-authorized="true" data-max-traces="15" data-toggle-shortcut="Alt+P" data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync"></script> <!-- HTML for static distribution bundle build --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>%(DocumentTitle)</title> <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="./swagger-ui.css"> <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> <style> html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } *, *:before, *:after { box-sizing: inherit; } body { margin: 0; background: #fafafa; } </style> %(HeadContent) </head> <body> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0"> <defs> <symbol viewBox="0 0 20 20" id="unlocked"> <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path> </symbol> <symbol viewBox="0 0 20 20" id="locked"> <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z" /> </symbol> <symbol viewBox="0 0 20 20" id="close"> <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z" /> </symbol> <symbol viewBox="0 0 20 20" id="large-arrow"> <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z" /> </symbol> <symbol viewBox="0 0 20 20" id="large-arrow-down"> <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z" /> </symbol> <symbol viewBox="0 0 24 24" id="jump-to"> <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z" /> </symbol> <symbol viewBox="0 0 24 24" id="expand"> <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" /> </symbol> </defs> </svg> <div id="swagger-ui"></div> <!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 --> <script> if (window.navigator.userAgent.indexOf("Edge") > -1) { console.log("Removing native Edge fetch in favor of swagger-ui's polyfill") window.fetch = undefined; } </script> <script src="./swagger-ui-bundle.js"></script> <script src="./swagger-ui-standalone-preset.js"></script> <script> window.onload = function () { var configObject = JSON.parse('%(ConfigObject)'); var oauthConfigObject = JSON.parse('%(OAuthConfigObject)'); // Apply mandatory parameters configObject.dom_id = "#swagger-ui"; configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]; configObject.layout = "StandaloneLayout"; // If oauth2RedirectUrl isn't specified, use the built-in default if (!configObject.hasOwnProperty("oauth2RedirectUrl")) configObject.oauth2RedirectUrl = window.location.href.replace("index.html", "oauth2-redirect.html"); // Build a system const ui = SwaggerUIBundle(configObject); // Apply OAuth config ui.initOAuth(oauthConfigObject); } </script> </body> </html>
<ItemGroup> <EmbeddedResource Include="SwaggerIndex.html" /> </ItemGroup>
-
在 Startup 中配置 MiniProfiler: 在 ConfigureServices 里面添加
services.AddMiniProfiler().AddEntityFramework()
, 在 Configure 里面添加app.UseMiniProfiler();
並配置 Swagger 的 IndexStream.public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); //Swagger services.AddSwaggerGen(options => { options.DescribeAllEnumsAsStrings(); options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "API Docs", Version = "v1", }); }); //Profiling services.AddMiniProfiler(options => options.RouteBasePath = "/profiler" ).AddEntityFramework(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); // profiling, url to see last profile check: http://localhost:56775/profiler/results app.UseMiniProfiler(); } app.UseSwagger(); app.UseSwagger().UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1"); // index.html customizable downloadable here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html // this custom html has miniprofiler integration c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("ORMDemo.EFWithRepository.SwaggerIndex.html"); }); app.UseMvc(); }
-
運行項目,MiniProfiler 監控頁面應該已經出現在 Swagger UI 頁面的左上角了。
-
倉儲模式和工作單元模式
倉儲模式(Repository)是用來解耦的(通過在數據訪問層和業務邏輯層之間創建抽象層)。
但倉儲只關注於單一聚合的持久化,而業務用例卻常常會涉及多個聚合的更改,為了確保業務用例的一致型,我們需要引入工作單元來管理多個聚合。
工作單元模式(unit of work)的作用就是在業務用例的操作中跟蹤對象的所有更改(增加、刪除和更新),並將所有更改的對象保存在其維護的列表中。在業務用例的終點,通過事務,一次性提交所有更改,以確保數據的完整性和有效性。總而言之,UOW協調這些對象的持久化及並發問題。
在 EF Core 中 DBContext 已經實現了工作單元模式,同時也比較容易更換統一的數據存儲介質(通過支持的數據庫驅動)。那么還有沒有必要在 EF Core 上面再封裝一層實現自己的倉儲和工作單元呢?
- 如果項目比較簡單,業務邏輯並不復雜。特別是在實現一些微服務的時候,每個項目(服務)都只負責一部分小的並且功能內聚的業務。這個時候或許保持代碼簡單最好,沒有必要過度設計。
- 當然,如果項目比較復雜,沒有采用微服務架構而是多個模塊都在一起的單體架構,可能同時需要多種數據存儲介質和途徑,用到了多種的數據訪問和持久化技術,那么可能就需要好好設計一個適合項目的倉儲和工作單元模式了。
下面實現一個簡單的倉儲和工作單元模式:
-
定義實體基類
public abstract class BaseEntity<TKey> { public virtual TKey Id { get; set; } }
-
定義倉儲基類
public interface IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext { Task<TEntity> GetByKeyAsync(TKey id); Task<IList<TEntity>> GetAsync( Expression<Func<TEntity, bool>> predicate = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, List<Expression<Func<TEntity, object>>> includes = null); Task<TEntity> AddAsync(TEntity entity); TEntity Update(TEntity entity); void Delete(TKey id); void Delete(TEntity entity); } public class EFRepository<TDbContext, TEntity, TKey> : IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext { protected readonly TDbContext _context; protected readonly DbSet<TEntity> dbSet; public EFRepository(TDbContext context) { this._context = context; this.dbSet = context.Set<TEntity>(); } public virtual async Task<TEntity> GetByKeyAsync(TKey id) { return await dbSet.FindAsync(id); } public virtual async Task<IList<TEntity>> GetAsync( Expression<Func<TEntity, bool>> predicate = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, List<Expression<Func<TEntity, object>>> includes = null) { IQueryable<TEntity> query = dbSet; if (includes != null) { query = includes.Aggregate(query, (current, include) => current.Include(include)); } if (orderBy != null) { query = orderBy(query); } if (predicate != null) { query = query.Where(predicate); } return await query.ToListAsync(); } public virtual async Task<TEntity> AddAsync(TEntity entity) { var result = await dbSet.AddAsync(entity); return result.Entity; } public virtual TEntity Update(TEntity entity) { AttachIfNot(entity); this._context.Entry(entity).State = EntityState.Modified; return entity; } public virtual void Delete(TKey id) { TEntity entity = dbSet.Find(id); Delete(entity); } public virtual void Delete(TEntity entity) { AttachIfNot(entity); dbSet.Remove(entity); } protected virtual void AttachIfNot(TEntity entity) { if (this._context.Entry(entity).State == EntityState.Detached) { dbSet.Attach(entity); } } }
可以根據需求擴展更多的方法。
-
定義工作單元基類
public interface IUnitOfWork<TDbContext> where TDbContext : DbContext { Task<int> SaveChangesAsync(); } public class UnitOfWork<TDbContext> : IUnitOfWork<TDbContext> where TDbContext : DbContext { private readonly TDbContext _dbContext; public UnitOfWork(TDbContext context) { _dbContext = context ?? throw new ArgumentNullException(nameof(context)); } public async Task<int> SaveChangesAsync() { return await _dbContext.SaveChangesAsync(); } }
-
定義 BloggingContext 並定義基於 BloggingContext 的倉儲基類和工作單元基類
public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new BlogConfiguration()); modelBuilder.ApplyConfiguration(new PostConfiguration()); } } public interface IBlogggingRepositoryBase<TEntity, TKey> : IRepository<BloggingContext, TEntity, TKey> where TEntity : BaseEntity<TKey> { } public class BlogggingRepositoryBase<TEntity, TKey> : EFRepository<BloggingContext, TEntity, TKey>, IBlogggingRepositoryBase<TEntity, TKey> where TEntity : BaseEntity<TKey> { public BlogggingRepositoryBase(BloggingContext dbContext) : base(dbContext) { } } public class BloggingUnitOfWork : UnitOfWork<BloggingContext> { public BloggingUnitOfWork(BloggingContext dbContext) : base(dbContext) { } }
-
在 Startup 的 ConfigureServices 里面注冊相關服務
public void ConfigureServices(IServiceCollection services) { var connectionString = @"Server=.;Database=BloggingWithRepository;Trusted_Connection=True;"; services.AddDbContext<BloggingContext>(option => option.UseSqlServer(connectionString)); services.AddScoped<BloggingUnitOfWork>(); services.AddTransient(typeof(IBlogggingRepositoryBase<,>), typeof(BlogggingRepositoryBase<,>)); }
這里 BloggingContext 和 UnitOfWork 的生命周期為 Scoped。
-
在 Controller 里面調用並測試
public class BlogsController : ControllerBase { private readonly IBlogggingRepositoryBase<Blog, int> _blogRepository; private readonly IBlogggingRepositoryBase<Post, int> _postRepository; private readonly BloggingUnitOfWork _unitOfWork; public BlogsController(IBlogggingRepositoryBase<Blog, int> blogRepository, IBlogggingRepositoryBase<Post, int> postRepository, BloggingUnitOfWork unitOfWork) { _blogRepository = blogRepository; _postRepository = postRepository; _unitOfWork = unitOfWork; } [HttpGet] public async Task<IActionResult> GetBlogs() { var blogs = await _blogRepository.GetAsync(); return Ok(blogs); } [HttpPost] public async Task<IActionResult> PostBlog([FromBody] Blog blog) { if (!ModelState.IsValid) { return BadRequest(ModelState); } //await _blogRepository.AddAsync(new Blog { Url = "http://sample.com/4", Rating = 0 }); //await _postRepository.AddAsync(new Post { Title = "Title4", Content = "BlogId_1 Post_3", BlogId = 1 }); var result = await _blogRepository.AddAsync(blog); await _unitOfWork.SaveChangesAsync(); return CreatedAtAction("GetBlog", new { id = blog.Id }, blog); } }
使用 EF Core(DB First)
EF Core 的 DB First 是通過 Scaffold-DbContext
命令根據已經存在的數據庫創建實體類和context類。
可以通過PM> get-help scaffold-dbcontext –detailed
查看命令的詳細參數
Scaffold-DbContext [-Connection] <String> [-Provider] <String> [-OutputDir <String>] [-ContextDir <String>] [-Context <String>] [-Schemas <String[]>] [-Tables <String[]>] [-DataAnnotations] [-UseDatabaseNames] [-Force]
[-Project <String>] [-StartupProject <String>] [<CommonParameters>]
使用之前創建的 blogging 數據庫簡單的測試一下:
-
新建一個項目,然后通過 Nuget 安裝 EF Core 引用
Install-Package Microsoft.EntityFrameworkCore.SqlServer Install-Package Microsoft.EntityFrameworkCore.Tools
-
執行命令創建實體
Scaffold-DbContext "Server=CD02SZV3600503\SQLEXPRESS;Database=BloggingWithRepository;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
執行成功后可以看到在 Models 文件夾下面創建的實體類和 Context 類。
源代碼
參考