【转载】从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置

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

前三部分弄完,咱们已经能够对内存数据进行CRUD的基本操做,而且能够在asp.net core 2中集成Nlog了。html

下面继续:java

Entity Framework Core 2.0

Entity Framework 是ORM(Object-Relational-Mapping)。ORM是一种让你可使用面向对象的范式对数据库进行查询和操做。git

简单的状况下,ORM能够把数据库中的表和Model对象一一映射起来;也有比较复杂的状况,ORM容许使用OO(面向对象)功能来作映射,例如:Person做为基类,Employee做为Person的派生类,他们俩能够在数据库中映射成一个表;或者在没有继承的状况下,数据库中的一个表可能和多个类有映射关系。github

EF Core 不是 EF6的升级版,这个你们应该知道,EF Core是轻量级、具备很好的扩展性的,而且是跨平台的EF版本。web

EF Core 目前有不少Providers,因此支持不少种数据库,包括:MSSQL,SQLite,SQL Compact,Postgres,MySql,DB2等等。并且还有一个内存的Provider,用于测试和开发。开发UWP应用的时候也可使用EF Core(用SQLite Provider)。sql

EF Core支持两种模式:数据库

Code First:简单理解为 先写C#(Model),而后生成数据库。json

Database First:如今数据库中创建表,而后生成C#的Model。windows

因为用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

相关文章
相关标签/搜索