【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (4) EF配置 【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (4) EF配置

【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (4) EF配置

Github源碼地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratchhtml

前三部分弄完,咱們已經能夠對內存數據進行CRUD的基本操做,而且能夠在asp.net core 2中集成Nlog了。git

下面繼續:github

Entity Framework Core 2.0

Entity Framework 是ORM(Object-Relational-Mapping)。ORM是一種讓你可使用面向對象的範式對數據庫進行查詢和操做。web

簡單的狀況下,ORM能夠把數據庫中的表和Model對象一一映射起來;也有比較複雜的狀況,ORM容許使用OO(面向對象)功能來作映射,例如:Person做爲基類,Employee做爲Person的派生類,他們倆能夠在數據庫中映射成一個表;或者在沒有繼承的狀況下,數據庫中的一個表可能和多個類有映射關係。sql

EF Core 不是 EF6的升級版,這個你們應該知道,EF Core是輕量級、具備很好的擴展性的,而且是跨平臺的EF版本。數據庫

EF Core 目前有不少Providers,因此支持不少種數據庫,包括:MSSQL,SQLite,SQL Compact,Postgres,MySql,DB2等等。並且還有一個內存的Provider,用於測試和開發。開發UWP應用的時候也可使用EF Core(用SQLite Provider)。json

EF Core支持兩種模式:windows

Code First:簡單理解爲 先寫C#(Model),而後生成數據庫。api

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,並運行:

種子數據進去了!

 

先寫到這吧!!!!

 

轉自:http://www.cnblogs.com/cgzl/p/7661805.html

相關文章
相關標籤/搜索