目錄css
Entity Framework Core 是微軟自家的 ORM 框架。做爲 .Net Core 生態中的一個重要組成部分,它是一個支持跨平臺的全新版本,用三個詞來概況 EF Core 的特色:輕量級、可擴展、跨平臺。html
目前 EF Core 支持的數據庫:git
新建一個 WebAPI 項目github
經過 Nuget 安裝 EF Core 引用web
// SQL Server Install-Package Microsoft.EntityFrameworkCore.SqlServer
其餘數據庫請查看:https://docs.microsoft.com/zh-cn/ef/core/providers/sql
添加實體數據庫
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; } }
添加數據庫上下文json
public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
有兩種方式配置數據庫鏈接,一種是註冊 Context 的時候提供 options。比較推薦這種方式。api
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; } }
經過 Nuget 引入EF Core Tool 的引用
Install-Package Microsoft.EntityFrameworkCore.Tools
若是須要使用 dotnet ef
命令, 請添加 Microsoft.EntityFrameworkCore.Tools.DotNet
生成遷移
打開Package Manager Console,執行命令Add-Migration InitialCreate
。更新遷移到數據庫
執行命令 Update-Database
。
若是執行成功,數據庫應該已經建立成功了。如今能夠測試剛纔建立的WebAPI應用了。
使用代碼
Database.Migrate();
能夠達到一樣的目的
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { Database.Migrate(); }
EF 根據對 Model 的配置生成表和字段,主要有三種配置方式:
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; } }
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
處理衝突的策略:
EF Core 使用如下方法執行 SQL 語句和存儲過程:
DbSet
DbSet<TEntity>.FromSql()
返回值爲IQueryable,能夠與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();
調用 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;
儘可能避免在循環時候使用延遲加載,會致使每次循環都去訪問數據庫。
直接經過一個實例測試一下:
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 服務器取到數據並加載到本地內存中。
EF Core 將遷移更新到生產環境可使用 Script-Migration
命令生成sql腳本,而後到生產數據庫執行.
此命令有幾個選項。
-From <String>
遷移應是運行該腳本前應用到數據庫的最後一個遷移。 若是未應用任何遷移,請指定 0(默認值)。-To <String>
遷移是運行該腳本後應用到數據庫的最後一個遷移。 它默認爲項目中的最後一個遷移。-Idempotent
此腳本僅會應用還沒有應用到數據庫的遷移。 若是不確知應用到數據庫的最後一個遷移或須要部署到多個可能分別處於不一樣遷移的數據庫,此腳本很是有用。有幾種方法能夠監視 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 是經過 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 類。
參考