第1部分: http://www.cnblogs.com/cgzl/p/7637250.html
第2部分:http://www.cnblogs.com/cgzl/p/7640077.html
第3部分:http://www.cnblogs.com/cgzl/p/7652413.html
Github源碼地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratch
前三部分弄完,我們已經可以對內存數據進行CRUD的基本操作,並且可以在asp.net core 2中集成Nlog了。
下面繼續:
Entity Framework Core 2.0
Entity Framework 是ORM(Object-Relational-Mapping)。ORM是一種讓你可以使用面向對象的范式對數據庫進行查詢和操作。
簡單的情況下,ORM可以把數據庫中的表和Model對象一一映射起來;也有比較復雜的情況,ORM允許使用OO(面向對象)功能來做映射,例如:Person作為基類,Employee作為Person的派生類,他們倆可以在數據庫中映射成一個表;或者在沒有繼承的情況下,數據庫中的一個表可能和多個類有映射關系。
EF Core 不是 EF6的升級版,這個大家應該知道,EF Core是輕量級、具有很好的擴展性的,並且是跨平台的EF版本。
EF Core 目前有很多Providers,所以支持很多種數據庫,包括:MSSQL,SQLite,SQL Compact,Postgres,MySql,DB2等等。而且還有一個內存的Provider,用於測試和開發。開發UWP應用的時候也可以使用EF Core(用SQLite Provider)。
EF Core支持兩種模式:
Code First:簡單理解為 先寫C#(Model),然后生成數據庫。
Database First:現在數據庫中建立表,然后生成C#的Model。
由於用asp.net core 2.0開發的項目基本都是新項目,所以建議使用Code First。
創建 Entity
Entity就是普通的C#類,就像Dto一樣。Dto是與外界打交道的Model,entity則不一樣,有一些Dto的計算屬性我們並不像保存在數據庫中,所以entity中沒有這些屬性;而數據從entity傳遞到Dto后某些屬性也會和數據庫里面的形式不一樣。
首先把我們原來的Product和Material這兩個Dto的名字重構一下,改成ProductDto和MaterialDto。
建立一個Entities文件夾,在里面建立Product.cs:
namespace CoreBackend.Api.Entities { public class Product { public int Id { get; set; } public string Name { get; set; } public float Price { get; set; } } }
DbContext
EFCore使用一個DbContext和數據庫打交道,它代表着和數據庫之間的一個Session,可以用來查詢和保存我們的entities。
DbContext需要一個Provider,以便能訪問數據庫(這里我們就用LocalDB吧)。
我們就建立一個DbContext吧(大一點的項目會使用多個DbContext)。建立MyContext並集成DbContext:
namespace CoreBackend.Api.Entities { public class MyContext : DbContext { public DbSet<Product> Products { get; set; } } }
這里我們為Product建立了一個類型為DbSet<T>的屬性,它可以用來查詢和保存實例(針對DbSet的Linq查詢語句將會被解釋成針對數據庫的查詢語句)。
因為我們需要使用這個MyContext,所以就需要先在Container中注冊它,然后就可以在依賴注入中使用了。
打開Startup.cs,修改ConfigureServices,添加這一句話:
services.AddDbContext<MyContext>();
使用AddDbContext這個Extension method為MyContext在Container中進行注冊,它默認的生命周期使Scoped。
但是它如何連接數據庫?這就需要連接字符串,我們需要為DbContext提供連接字符串,這里有兩種方式。
第一種是在MyContext中override OnConfiguring這個方法:
namespace CoreBackend.Api.Entities { public class MyContext : DbContext { public DbSet<Product> Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("xxxx connection string"); base.OnConfiguring(optionsBuilder); } } }
其中的參數optionsBuilder提供了一個UseSqlServer()這個方法,它告訴Dbcontext將會被用來連接Sql Server數據庫,在這里就可以提供連接字符串,這就是第一種方法。
第二種方法:
先大概看一下DbContext的源碼的定義:
namespace Microsoft.EntityFrameworkCore { public class DbContext : IDisposable, IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbContextPoolable { public DbContext([NotNullAttribute] DbContextOptions options);
有一個Constructor帶有一個DbContextOptions參數,那我們就在MyContext種建立一個Constructor,並overload這個帶有參數的Constructor。
namespace CoreBackend.Api.Entities { public class MyContext : DbContext { public MyContext(DbContextOptions<MyContext> options) :base(options) { } public DbSet<Product> Products { get; set; } } }
這種方法相對第一種的優點是:它可以在我們注冊MyContext的時候就提供options,顯然這樣做比第一種override OnConfiguring更合理。
然后返回Startup:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #if DEBUG services.AddTransient<IMailService, LocalMailService>(); #else services.AddTransient<IMailService, CloudMailService>(); #endif var connectionString = @"Server=(localdb)\MSSQLLocalDB;Database=ProductDB;Trusted_Connection=True"; services.AddDbContext<MyContext>(o => o.UseSqlServer(connectionString)); }
使用AddDbContext的另一個overload的方法,它可以帶一個參數,在里面調用UseSqlServer。
關於連接字符串,我是用的是LocalDb,實例名是MSSQLLocalDB。可以在命令行查詢本機LocalDb的實例,使用sqllocaldb info:
也可以通過VS的Sql Server Object Explorer查看:
連接字符串中的ProductDb是數據庫名;連接字符串的最后一部分表示這是一個受信任的連接,也就是說使用了集成驗證,在windows系統就是指windows憑證。
生成數據庫
因為我們使用的是Code First,所以如果還沒有數據庫的話,它應該會自動建立一個數據庫。
打開MyContext:
public MyContext(DbContextOptions<MyContext> options) :base(options) { Database.EnsureCreated(); }
這個Constructor在被依賴注入的時候會被調用,在里面寫Database.EnsureCreated()。其中Database是DbContext的一個屬性對象。
EnsureCreated()的作用是,如果有數據庫存在,那么什么也不會發生。但是如果沒有,那么就會創建一個數據庫。
但是現在就運行的話,並不會創建數據庫,因為沒有創建MyContext的實例,也就不會調用Constructor里面的內容。
那我們就建立一個臨時的Controller,然后注入MyContext,此時就調用了MyContext的Constructor:
namespace CoreBackend.Api.Controllers { [Route("api/[controller]")] public class TestController: Controller { private MyContext _context; public TestController(MyContext context) { _context = context; } [HttpGet] public IActionResult Get() { return Ok(); } } }
使用Postman訪問Get這個Action后,我們可以從Debug窗口看見一些創建數據庫和表的Sql語句:
然后我們查看一下Sql Server Object Explorer:
我們可以看到數據庫建立好了,里面還有dbo.Products這個表。
Database.EnsureCreated()確實可以保證創建數據庫,但是隨着代碼不斷被編寫,我們的Model不斷再改變,數據庫應該也隨之改變,而EnsureCreated()就不夠了,這就需要遷移(Migration)。
不過遷移之前,我們先看看Product這個表的具體字段屬性:
Product的Id作為了主鍵,而Name這個字符串的長度是max,而Price沒有精度限制,這樣不行。我們需要對Model生成的表的字段進行限制!
解釋一下:Product這個entity中的Id,根據約定(Id或者ProductId)會被視為映射表的主鍵,並且該主鍵是自增的。
如果不使用Id或者ProductId這兩個名字作為主鍵的話,我們可以通過兩種方式把該屬性設置成為主鍵:Data Annotation注解和Fluet Api。我只在早期使用Data Annotation,后來一直使用Fluent Api,所以我這里只介紹Fluent Api吧。
Fluet Api
針對Product這個entity,我們要把它映射成一個數據庫的表,所以針對每個屬性,可能需要設定一些限制,例如最大長度,是否必填等等。
針對Product,我們可以在MyContext里面override OnModelCreating這個方法,然后這樣寫:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasKey(x => x.Id);
modelBuilder.Entity<Product>().Property(x => x.Name).IsRequired().HasMaxLength(50);
modelBuilder.Entity<Product>().Property(x => x.Price).HasColumnType("decimal(8,2)");
}
第一行表示設置Id為主鍵(其實我們並不需要這么做)。然后Name屬性是必填的,而且最大長度是50。最后Price的精度是8,2,數據庫里的類型為decimal。
fluent api有很多方法,具體請查看文檔:https://docs.microsoft.com/en-us/ef/core/modeling/
然后,我們就會發現一個嚴重的問題。如果項目里面有很多entity,那么所有的fluent api配置都需要寫在OnModelCreating這個方法里,那太多了。
所以我們改進一下,使用IEntityTypeConfiguration<T>。建立一個叫ProductConfiguration的類:
public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.HasKey(x => x.Id); builder.Property(x => x.Name).IsRequired().HasMaxLength(50); builder.Property(x => x.Price).HasColumnType("decimal(8,2)"); } }
把剛才在MyContext里寫的配置都移動到這里,然后修改一些MyContext的OnModelCreating方法:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new ProductConfiguration()); }
就是把ProductConfiguration里面寫的配置加載進來,和之前的效果是一樣的。
但是項目中如果有很多entities的話也需要寫很多行代碼,更好的做法是寫一個方法,可以加載所有實現了IEntityTypeConfiguration<T>的實現類。在老版的asp.net web api 2.2里面有一個方法可以從某個Assembly加載所有繼承於EntityTypeConfiguration的類,但是entity framework core並沒有提供類似的方法,以后我們自己寫一個吧,現在先這樣。
然后把數據庫刪掉,重新生成一下數據庫:
很好!
遷移 Migration
隨着代碼的更改,數據庫也會跟着變,所有EnsureCreated()不滿足要求。migration就允許我們把數據庫從一個版本升級到另一個版本。那我們就研究一下,首先把數據庫刪了,然后創建第一個遷移版本。
打開Package Manager Console,做個遷移 Add-Migration xxx:
Add-Migration 然后接着是一個你起的名字。
然后看一下VS的Solution Explorer 會發現生成了一個Migrations目錄:
里面有兩個文件,一個是Snapshot,它是目前entity的狀態:
namespace CoreBackend.Api.Migrations { [DbContext(typeof(MyContext))] partial class MyContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "2.0.0-rtm-26452") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("CoreBackend.Api.Entities.Product", b => { b.Property<int>("Id") .ValueGeneratedOnAdd(); b.Property<string>("Name") .IsRequired() .HasMaxLength(50); b.Property<float>("Price") .HasColumnType("decimal(8,2)"); b.HasKey("Id"); b.ToTable("Products"); }); #pragma warning restore 612, 618 } } }
這就是當前Product這個Model的狀態細節,包括我們通過Fluent Api為其添加的映射限制等。
另一個文件是xxxx_ProductInfoDbInitialMigration,下划線后邊的部分就是剛才Add-Migration命令后邊跟着的名字參數。
namespace CoreBackend.Api.Migrations { public partial class ProductInfoDbInitialMigration : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Products", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false), Price = table.Column<float>(type: "decimal(8,2)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Products", x => x.Id); }); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "Products"); } } }
這里面包含着migration builder需要的代碼,用來遷移這個版本的數據庫。里面有Up方法,就是從當前版本升級到下一個版本;還有Down方法,就是從下一個版本再退回到當前版本。
我們也可以不使用 Add-Migration命令,手寫上面這些代碼也行,我感覺還是算了吧。
另外還有一件事,那就是要保證遷移migration都有效的應用於數據庫了,那就是另一個命令 Update-Database。
先等一下,我們也可以使用代碼來達到同樣的目的,打開MyContext:
public MyContext(DbContextOptions<MyContext> options) : base(options) { Database.Migrate(); }
把之前的EnsureCreated改成Database.Migrate(); 如果數據庫還沒刪除,那就最后刪除一次。
運行,並除法TestController:
然后會看見Product表,除此之外還有一個__EFMigrationHistory表,看看有啥:
這個表里面保存了哪些遷移已經被應用於這個數據庫了。這也保證了Database.Migrate()或者Update-database命令不會執行重復的遷移migration。
我們再弄個遷移,為Product添加一個屬性:
namespace CoreBackend.Api.Entities { public class Product { public int Id { get; set; } public string Name { get; set; } public float Price { get; set; } public string Description { get; set; } } public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.HasKey(x => x.Id); builder.Property(x => x.Name).IsRequired().HasMaxLength(50); builder.Property(x => x.Price).HasColumnType("decimal(8,2)"); builder.Property(x => x.Description).HasMaxLength(200); } } }
執行Add-Migration后,會在Migrations目錄生成了一個新的文件:
namespace CoreBackend.Api.Migrations { public partial class AddDescriptionToProduct : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<string>( name: "Description", table: "Products", type: "nvarchar(200)", maxLength: 200, nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "Description", table: "Products"); } } }
然后這次執行Update-Database命令:
加上verbose參數就是顯示執行過程的明細而已。
不用運行,看看數據庫:
Description被添加上了,然后看看遷移表:
目前差不太多了,但還有一個安全隱患。它是:
如何安全的保存敏感的配置數據,例如:連接字符串
保存連接字符串,你可能會想到appSettings.json,但這不是一個好的想法。在本地開發的時候還沒有什么問題(使用的是集成驗證),但是你要部署到服務器的時候,數據庫連接字符串可能包括用戶名和密碼(Sql Server的另一種驗證方式)。加入你不小心把appSettings.json或寫到C#里面的連接字符串代碼提交到了Git或TFS,那么這個用戶名和密碼包括服務器的名稱可能就被暴露了,這樣做很不安全。
我們可以這樣做,首先針對開發環境(development environment)把C#代碼中的連接字符串拿掉,把它放到appSettings.json里面。然后針對正式生產環境(production environment),我們使用環境變量來保存這些敏感數據。
開發環境:
appSettings.json:
{ "mailSettings": { "mailToAddress": "admin__json@qq.com", "mailFromAddress": "noreply__json@qq.com" }, "connectionStrings": { "productionInfoDbConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=ProductDB;Trusted_Connection=True" } }
Startup.cs:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #if DEBUG services.AddTransient<IMailService, LocalMailService>(); #else services.AddTransient<IMailService, CloudMailService>(); #endif var connectionString = Configuration["connectionStrings:productionInfoDbConnectionString"]; services.AddDbContext<MyContext>(o => o.UseSqlServer(connectionString)); }
然后你可以設斷點看看connectionString的值。目前項目的環境變量是Production,先改成Development:
然后斷點調試:
可以看到這兩個JsonConfigurationProvider就是appSettings的兩個文件的配置。
這個就是appSettings.json,里面包含着我們剛才添加的連接字符串。
由於當前是Development環境,所以如果你查看另外一個JsonConfigurationProvider的話,會發現它里面的值是空的(Data數是0).
所以沒有問題。
生產環境:
在項目的屬性--Debug里面,我們看到了環境變量:
而這個環境變量,我們可以在程序中讀取出來,所以可以在這里添加連接字符串:
注意它的key,要和appSettings.json里面的整體結構一致;Value呢應該是給一個服務器的數據庫的字符串,這里就隨便弄個假的吧。別忘了把Development改成Production。
然后調試一下:
沒錯。如果你仔細調試一下看看的話:就會從EnvironmentVariablesConfigurationProvider的第64個找到我們剛才寫到連接字符串:
但是還沒完。
打開項目的launchSettings.json:
你會發現:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:60835/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "connectionStrings:productionInfoDbConnectionString": "Server=.;Database=ProductDB;UserId=sa;Password=pass;", "ASPNETCORE_ENVIRONMENT": "Production" } }, "CoreBackend.Api": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:60836/" } } }
連接字符串在這里。這個文件一般都會源碼控制給忽略,也不會在發布的時候發布到服務器。那么服務器怎么讀取到這個連接字符串呢???
看上面調試EnvironmentVariablesConfigurationProvider的值,會發現里面有幾十個變量,這些基本都不是來自launchSettings.json,它們是從系統層面上定義的!!
這回我們這樣操作:
把launchSettings里面的連接字符串去掉:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:60835/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Production" } }, "CoreBackend.Api": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:60836/" } } }
然后這里自然也就沒有了:
現在任何json文件都沒有敏感信息了。
現在我們要把連接字符串添加到系統變量中。
在win10搜索框輸入 envi:
然后點擊上面的結果:
點擊環境變量:
這里面上邊是用戶的變量,下面是系統的變量,這就是剛才EnvironmentVariableConfigurationProvider里面調試出來的那一堆環境變量。
而這個地方就是在你應該服務器上添加連接字符串的地方。再看一下調試:
Environment的Provider在第4個位置,appSettings.production.json的在第3個位置。也就是說如果appSettings.Product.json和系統環境變量都有一樣Key的連接字符串,那么程序會選擇系統環境變量的值,因為它是后邊的配置會覆蓋前邊的配置。
在系統環境變量中添加:
然后調試運行(需要重啟VS,以便新添加的系統環境變量生效):
嗯,沒問題!
種子數據 Seed Data
目前EF Core還沒有內置的方法來做種子數據。那么自己寫:
建立一個MyContextExtensions.cs:
namespace CoreBackend.Api.Entities { public static class MyContextExtensions { public static void EnsureSeedDataForContext(this MyContext context) { if (context.Products.Any()) { return; } var products = new List<Product> { new Product { Name = "牛奶", Price = 2.5f, Description = "這是牛奶啊" }, new Product { Name = "面包", Price = 4.5f, Description = "這是面包啊" }, new Product { Name = "啤酒", Price = 7.5f, Description = "這是啤酒啊" } }; context.Products.AddRange(products); context.SaveChanges(); } } }
這是個Extension method,如果數據庫沒有數據,那就弄點種子數據,AddRange可以添加批量數據到Context(被Context追蹤),但是到這還沒有插入到數據庫。使用SaveChanges會把數據保存到數據庫。
然后再Startup的Configure方法中調用這個method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, MyContext myContext) { // loggerFactory.AddProvider(new NLogLoggerProvider()); loggerFactory.AddNLog(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(); } myContext.EnsureSeedDataForContext(); app.UseStatusCodePages(); app.UseMvc(); }
首先注入MyContext,然后調用這個extension method。
然后把系統環境變量中的連接字符串刪了把,並且把項目屬性Debug中改成Development,這時候需要重啟VS,因為一般環境變量是在軟件啟動的時候附加到其內存的,軟件沒關的情況下如果把系統環境變量給刪了,在軟件的內存中應該還是能找到該環境變量,所以軟件得重啟才能獲取最新的環境變量們。重啟VS,並運行:
種子數據進去了!
先寫到這吧!!!!