.Net Core个人笔记

目录

前言

学习Net Core的个人笔记,记录

建议看微软官方文档,看不懂的查一下 教程:ASP.NET Core 入门

IOC注册

Startup类中的ConfigureServices方法是用于服务注册IOC

ConfigureServices这个方法是用于服务注册的,服务就是IOC里面的类

三种生命周期

IOC容器内的服务有三种生命周期

  1. Transient:每次请求都会创建一个新的实例
  2. Scoped:每次Web请求都会创建一个实例
  3. Singleton:一旦实例被创建,一直使用,直到应用停止

如何注册一个IOC服务

我们有一个类和一个接口,接口的实现类,这里不写,注册如下

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IStudentService,StudentService>();
        }

写了一个单例的服务,已经注入了,调用后续再写

.Net Core部署IIS之后500错误

我犯了一个极其傻逼的错误,.net Core是有一个生产环境和一个开发环境的,我部署之后使用的生产环境,可我的数据库都在开发环境上......我真是🐷

管道和中间件

示意图

下图很经典,用户的请求需要经过管道,管道内部可以写中间件,如果你什么都不写,那么请求返回的就是固定的,你加了中间件,就会有变化,比如权限验证,身份验证,MVC处理等中间件

管道方法

Startup类里面的Configure方法就是管道的方法,可以在里面写中间件

中间件

app.Run就是运行的一个中间件,现在我们写一个新的中间件

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("许嵩!");
            });

运行,可以发现,还是Hello World,根本没有许嵩,因为中间件根本没往下执行,可以这样设置

app.Use(async (context,next) =>
            {
                await context.Response.WriteAsync("Hello World!");
                await next();
            });
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("许嵩!");
            });

加了一个next,然后执行await next(); 这样就会执行按照顺序的下一个中间件

加日志观看

        public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILogger<Startup> logger)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.Use(async (context,next) =>
            {
                logger.LogInformation("管道1开启");
                await context.Response.WriteAsync("Hello World!");
                await next();
                logger.LogInformation("管道1结束");
            });
            app.Run(async (context) =>
            {
                logger.LogInformation("管道2开启");
                await context.Response.WriteAsync("许嵩!");
                logger.LogInformation("管道2结束");

            });
        }

加了一个日志ILogger,这样再运行,注意这次运行不选择IIS了,我们选择Kestrel服务器,就是你的解决方案同名的那个,运行,可以查看日志

使用MVC

MVC服务注入

直接新建一个控制器Controller,你会发现,Controller没有引入,无法使用.

.net Core和.net Framework不一样,.net MVC写MVC直接就可以, .net Core需要注册一下,刚好使用了上面的IOC服务注册,还是在Startup类中的ConfigureServices写:

services.AddMvc();

MVC管道调用

管道调用的时候可以加一个路由

app.UseMvc(route =>
{
    route.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
});

MVC文件夹以及代码创建

Controllers,Models,Views三个文件夹的创建

新建HomeController,添加Index视图

    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
@{
    ViewData["Title"] = "Index";
}

<h1>我的第一个.Net Core Web项目</h1>


结果

三种环境配置

有三种官方给的环境,分别是

Development(开发)、Staging (分阶段)和 Production(生产)

更改环境变量,点击项目,右键属性,选择调试,更改,如图

在Properties下的launchSettings.json可以更改不同环境的参数配置

{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:13335",
      "sslPort": 44347
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "StudyNetCore": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Startup可以判断环境

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
if (env.IsProduction())
{
    System.Console.WriteLine("...");
}
if (env.IsStaging())
{
    System.Console.WriteLine("...");
}
if (env.IsEnvironment("自定义环境"))
{
    System.Console.WriteLine("...");
}

使用HTTPS

在Startup类中注入HTTPS服务,并设置管道

在ConfigureServices类中注入

services.AddHttpsRedirection(option=> {
    option.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
    option.HttpsPort = 5001;
});

在Configure方法里面使用HTTPS管道,注意HTTPS管道必须在MVC管道之前,否则没意义了就

app.UseHttpsRedirection();

在launchSettings.json里面设置启动url

改成这样就可以,加了一个launchUrl,初始值为http://localhost:5000/Home/Index

这样

{
  "profiles": {
    "StudyNetCore": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000/Home/Index",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

EntityFrameWork Core的入门使用

真是一波三折~我开始就遇到报错了

Build failed.

这个报错是看看你的项目能不能编译成功,如果有报错请解决报错

严重性代码说明项目文件行禁止显示状态错误 MSB3541 Files 的值“<<<<<<< HEAD”无效。路径中具有非法字符。 StudyNetCore D:\Programs\VisualStudio2019\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets

这个错误不知道是啥,但是可以找出来是MVC项目的错,所以我把MVC项目的obj文件夹下面的全删了,然后重新编译一次就可以了

Unable to create an object of type 'MyContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

这个报错的原因是,需要把默认项目改成MVC的,才可以执行 Add-Migration InitialCreate,我也不知道为啥😠

开始EF Core入门

首先需要新建三个项目,一个是主项目,我建的是MVC

一个是Model类库,一个是操作EF Core的迁移类库

如图,我建了三个,DB是来放EF Core迁移文件的,DomainModels是放领域模型的,下面的MVC是业务

第一步,新建几个Model

我为了测试,在DomainModels下新建了一个Blog

using System;
using System.Collections.Generic;

namespace DomainModels
{
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
        public List<Post> Posts { get; set; }
    }
}

注意,一定要有主键,默认有Id或者XXId的都是主键

第二步,新建EF Core迁移

在DB里面使用NuGet引用EF Core,如下

  1. Microsoft.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.Design
  3. Microsoft.EntityFrameworkCore.SqlServer
  4. Microsoft.EntityFrameworkCore.Tools

新建DBContext的继承类,我的是这样的

using DomainModels;
using Microsoft.EntityFrameworkCore;
using System;

namespace DB
{
    public class MyContext:DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
 optionsBuilder.UseSqlServer("Server=.;Database=EFCore;Trusted_Connection=True;");
//如果是带用户名密码的这样写   optionsBuilder.UseSqlServer("server=192.168.3.8;uid=sa;pwd=123;database=VaeDB;");

        }
        public DbSet<Blog> Blogs { get; set; }
    }
}

开始迁移,在视图->其他窗口->包管理控制台

首先输入: Add-Migration InitialCreate 后面的是名称,随意写,我写的InitialCreate

然后执行迁移文件:Update-Database

等待一会,你就会发现本地的数据库里面已经有了EFCore数据库和Blogs数据表了

第三步,简易的增删改查

在MVC项目里面新建了一个Controller

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DB;
using DomainModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace StudyNetCore.Controllers
{
    public class EFCoreController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
        public void Add()
        {
            using (var context = new MyContext())
            {
                var blog = new Blog {
                    Url = "http://sample.com",
                    Rating=98
                };
                context.Blogs.Add(blog);
                context.SaveChanges();
            }
        }
        public void Remove()
        {
            using (var context = new MyContext())
            {
                var blog = context.Blogs.Single(b => b.BlogId == 2);
                context.Blogs.Remove(blog);
                context.SaveChanges();
            }
        }
        public void Update()
        {
            using (var context = new MyContext())
            {
                var blog = context.Blogs.Single(b => b.BlogId == 1);
                blog.Url = "http://www.vae.com";
                context.SaveChanges();
            }
        }
        public void Select()
        {
            using (var context = new MyContext())
            {
                var blogs = context.Blogs.ToList();
                Console.WriteLine(blogs);
            }
        }
    }
}

运行项目,输入对应的url,成功操作了数据

EF Core的数据库连接字符串写在配置文件中

上面的数据库配置文件是写在MyContext里面的,这样不合适,所以我写在了json文件里

appsettings.json

找到appsettings.json,在里面加上

  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=EFCore;Trusted_Connection=True;"
  }

Statrtup注入

在Startup里面的ConfigureServices方法里面写

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; set; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<MyContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddSingleton<IStudentService,StudentService>();
            services.AddMvc();

MyContext修改

    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options)
            : base(options)
        {
        }

        public DbSet<Blog> Blogs { get; set; }

    }

使用EFCore

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DB;
using DomainModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace StudyNetCore.Controllers
{
    public class EFCoreController : Controller
    {
        private readonly MyContext _myContext;
        public EFCoreController(MyContext myContext)
        {
            _myContext = myContext;
        }

        public IActionResult Index()
        {
            return View();
        }
        public void Add()
        {
            var blog = new Blog
            {
                Url = "http://sample.com",
                Rating = 98
            };
            _myContext.Blogs.Add(blog);
            _myContext.SaveChanges();
        }
        public void Remove()
        {
            var blog = _myContext.Blogs.Single(b => b.BlogId == 2);
            _myContext.Blogs.Remove(blog);
            _myContext.SaveChanges();
        }
        public void Update()
        {
            var blog = _myContext.Blogs.Single(b => b.BlogId == 1);
            blog.Url = "http://www.vae.com";
            _myContext.SaveChanges();
        }
        public IActionResult Select()
        {
            var list = _myContext.Blogs.ToList();
            Console.WriteLine(list);
            return View();
        }
    }
}

EntityModel和ViewModel的转化使用 (简单的推荐使用,复杂的推荐使用AutoMapper)

和数据库表字段对应的就是EntityModel,和视图对应的就是ViewModel

我新建一个EntityModel,如下

public class Student
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDay { get; set; }
}

可以看到,学生有两个名字,还有出生年月日,但是我页面上只想看到一个名字和年龄,这就需要处理一下了,ViewModel如下

public class HomeIndexViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

转化如下:

private readonly StudentService _studentService=new StudentService();

public IActionResult Index()
{
    List<Student> studentList = _studentService.getStudentList();
    var vms = studentList.Select(x => new HomeIndexViewModel
    {
        Name = x.FirstName + x.LastName,
        Age = DateTime.Now.Subtract(x.BirthDay).Days / 365
    });
    return View(vms);
}

视图使用如下:

@model IEnumerable<StudyNetCore.ViewModels.HomeIndexViewModel>
@{
    ViewData["Title"] = "Index";
}

<h1>我的第一个.Net Core Web项目</h1>

<h1>Student的ViewModel数据展示</h1>
<ul>
    @foreach (var item in Model)
    {
        <li>@item.Name - @item.Age</li>
    }
</ul>

@model是指令,只是为了让@Model有智能提示

结果如下:

使用AutoMapper (推荐)

小白扫盲:所谓的Model就是类🐷

在开发中,我们经常会使用到两种Model,一种是EntityModel,一种是ViewModel

  1. EntityModel:也称之为实体Model,这种Model的字段和数据库表的字段是一一对应的
  2. ViewModel:也称之为视图Model,这种Model是专门为视图这种表现层展示和接受数据用的

Model为什么要区分Entity和View?

EntityModel当然可以搞一个扩展字段充当ViewModel使用,但是EntityModel的主要职责是存储读取数据库表,它不在乎业务逻辑

ViewModel只在乎视图层面上的数据逻辑业务,不在乎数据的存储和读取,这就是分层架构的分层思想

举个例子,公司有不同的部门员工,这就是分工不同,我是软件部的程序员,职责就是开发软件,业务部门的同事,职责是和客户沟通

业务部同事忙碌的时候,我当然可以去帮他接个电话啥的,但这不是我的职责,我不应该做这样的事,公司招我也不是让我做这个的

所以,你硬是拿Entity去当ViewModel使用也能用,但是不建议

而且还有一个理由,EntityModel只关心数据的存取,ViewModel只关心表现层视图层的数据的提交和展现,所以ViewModel可以做数据后台校验

正式在.net Core中使用AutoMapper

上面的都是为什么,明白为什么之后我们开始实际操作了,很简单

引入AutoMapper的相关包

打开Nuget,搜索

AutoMapper

AutoMapper.Extensions.Microsoft.DependencyInjection

创建映射文件

你的所有的EntityModel和ViewModel之间的映射关系,都在这个文件

可以看到我的写法,两个model之间需要映射的字段名一样的话就直接写,如果有字段名不一样的话就加上ForMember自己映射

using AutoMapper;
using DomainModels;
using ViewModels;

namespace Web.AutoMapper
{
    public class AutoMapperProfile : Profile
    {
        public AutoMapperProfile()
        {
            #region Movie
            //CreateMap<Movie, MovieViewModel>(); 字段完全一致就可以这样写
            CreateMap<Movie, MovieViewModel>()
                .ForMember(dest => dest.Info, opt => opt.MapFrom(src => src.Title + src.Genre))
                .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.ReleaseDate));

            #endregion
        }
    }
}

AutoMapper注入

在Startup.cs里面注入

//AutoMapper注入
services.AddAutoMapper(typeof(Startup));

使用AutoMapper映射

var movies = _context.Movie.ToList();
if (movies?.Count() > 0)
{
    foreach (var movie in movies)
    {
        MovieViewModel movievm = _mapper.Map<MovieViewModel>(movie);
        System.Console.WriteLine("不错哦,打断点看到映射成功");
    }
}

输入Model和防止重复post

前端HTML输入Model传给后台我知道,待补充

待补充

然后页面重复刷新会造成post的重复提交,解决办法就是post提交一次之后就立即重定向,如下

return RedirectToAction(nameof(Detail));

Model数据验证

一个输入的表单,例如我输入用户的姓名,年龄,手机号,邮箱,密码等等,这个对应的Model需要做数据验证

可能有人会问,前端我验证不就得了,前端数据填写不合法的时候就报错,后端的Model还验证什么呢?我以前也是这么想的

直到我知道了PostMan.....

有很多方法可以绕过你的前端验证的,如果你没加后端验证,有人直接传入非法数据就不安全了

所以,数据验证.前后端都需要做,双重保险

类似这样

    public class Student
    {
        [Required]
        public int Id { get; set; }
        [StringLength(20)]
        public string FirstName { get; set; }
        public string LastName { get; set; }
        [DataType(DataType.Date)]
        public DateTime BirthDay { get; set; }
    }

下面列了一些,更多的用到再更新

[Required]//必须数据
[StringLenght(100)]//最大长度100
[Range(0,999)]//取值范围是0-999
[DateType(DataType.Date)]//要求此数据必为日期类型
[CreaitCard]//信用卡
[Phone]//电话号码
[EmailAddress]//邮箱地址
[DataType(DataType.Password)] //密码
[Url]//必须是url链接
[Compare]//比较数据是否相同

例子

public class Movie
{
    public int ID { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; }
}
  • RequiredMinimumLength 特性表示属性必须有值;但用户可输入空格来满足此验证。
  • RegularExpression 特性用于限制可输入的字符。 在上述代码中,即“Genre”(分类):
    • 只能使用字母。
    • 第一个字母必须为大写。 不允许使用空格、数字和特殊字符。
  • RegularExpression“Rating”(分级):
    • 要求第一个字符为大写字母。
    • 允许在后续空格中使用特殊字符和数字。 “PG-13”对“分级”有效,但对于“分类”无效。
  • Range 特性将值限制在指定范围内。
  • StringLength 特性使你能够设置字符串属性的最大长度,以及可选的最小长度。

View

_Layout.cshtml

这个是母版页,有两个地方需要说明

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

第一个@ViewBag.Title 子页面可以这样写,这样就标题映射过去了

@{
    ViewBag.Title="Index";
}

下面的@RenderBody()就是你子页面展现的地方

还可以写一个母版页节点,让子页面去加载,例如

@RenderSection("Footer",required:false)

这样子页面就可以使用

@section Footer{
    <h3>大家好,我是脚</h3>
}

required:false是不是每个页面必须的,如果不写,每个子页面都得加载Footer节点

_ViewStart.cshtml

这个是每个页面加载之前都必须先加载的页面,通常直接放在Views的文件夹下面,这样就可以对所有的页面起作用了.如果把_ViewStart.cshtml放在了Home文件夹下面,那么仅仅对Home文件夹下的页面起作用

例如,我们可以把每个页面都有的东西放在_ViewStart.cshtml里面

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>都得先加载我</h1>

_ViewImports.cshtml

有时候我们会在视图里面用到自己写的类,这个时候我们通常是直接写全引用,或者在页面上写@using

但是,每一个页面都写重复的@using这样就不好了,可以统一的在_ViewImports.cshtml里写@using

这样每个页面直接写类的名称就可以了,比如

@using StudyNetCore.Models;

通常放在Views文件夹下

_PartialView.chhtml

这个是分部视图,多个页面都用到的HTML可以放到这里面,通常放在Share文件夹下

@model IEnumerable<HomeIndexViewModel>

<h1>我是分部视图</h1>

<ul>
    @foreach (var item in Model)
    {
        <li>@item.Name</li>
        <li>@item.Age</li>
    }
</ul>

调用的时候也很简单,输入名字传入Model就可以了

@Html.Partial("_PartialView",Model)
<partial name="_PartialView" for="HomeIndexViewModel" />

这两种方式都可以,但是推荐使用TagHelper方式

缺点,分部视图PartialView的缺点就是,这个Model必须是调用者传过来的,不能自己去查询加载数据,下面的ViewComponents就很好的解决了这个问题

ViewConponents

暂时不写,感觉略麻烦,感觉可以使用ViewBag代替

Identity:身份验证

两个主要的类先了解一下

  • UserManager:操作用户,例如创建用户,删除用户等

  • SignInManager:对用户进行身份验证

使用TagHelper

在_ViewImports.cshtml里面加上

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

然后页面就可以直接调用,如

<a asp-controller="Home" asp-action="data">还是点我,TagHelper类型的</a>

为什么使用TagHelper?因为改变路由之后这些会自动映射

字段重命名DisplayNameFor和Display

Model和数据库的字段都是一一对应的,但是我有一个这样的名字,如下

public DateTime ReleaseDate { get; set; }

ReleaseDate这个名字显然不合适,加一个空格就好多了,可以这样写

[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }

前端显示就使用@Html.DisplayNameFor,如下

<th>
    @Html.DisplayNameFor(model => model.ReleaseDate)
</th>

这样一来显示的就是有空格的名字了

这样讲一下@Html.DisplayNameFor就是现实名字,@Html.DisplayFor就是显示数据

我后台传入的是一个List,如下

public IActionResult Test()
{
    return View(_context.Movie.ToList());
}

前端接受使用model,然后处理的时候可以使用如下

@model IEnumerable<DomainModels.Movie>
@{
    ViewData["Title"] = "Test";
}

<h1>我就是Movie下面的一个Test页面</h1>

<div>
    <ul>
        @foreach (var item in Model)
        {
        <li>
            名字是: @Html.DisplayNameFor(model => model.Title)  |  内容是: @Html.DisplayFor(modelItem => item.Title)
            类别是: @Html.DisplayNameFor(model => model.Genre)  |  内容是: @Html.DisplayFor(modelItem => item.Genre)
            价格是: @Html.DisplayNameFor(model => model.Price)  |  内容是: @Html.DisplayFor(modelItem => item.Price)
        </li>
        }
    </ul>
</div>

讲一下标题可以使用model => model.Title

内容的话必须使用modelItem => item.Title,其中前面的modelItem 随意写,后面的 item.Title是必须使用的

WebAPI

这个WebAPI和MVC有什么区别呢?其实他俩乍一看很像,都是控制器加Action的模式

但是我觉得最大的区别就在于,MVC是有视图的,这个框架包含了很多东西

WebAPI呢根本就没有视图这个东西,所以内容比较纯净,就是单纯的操作数据

很明显的区别就是一个继承的是Controller,一个继承的是ApiController

新建WebAPI

这个实在没什么讲的,直接新建即可,使用EF Core来操作数据库,贴几个代码看看

using System.Collections.Generic;
using System.Linq;
using DB;
using DomainModels;
using Microsoft.AspNetCore.Mvc;

namespace WebAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemController : ControllerBase
    {
        private readonly MyContext _context;
        public TodoItemController(MyContext context)
        {
            _context = context;
        }

        // GET: api/TodoItem
        [HttpGet]
        public IEnumerable<TodoItem> Get()
        {
            return _context.TodoItem.ToList();
        }

        // GET: api/TodoItem/5
        [HttpGet("{id}", Name = "Get")]
        public TodoItem Get(int id)
        {
            return _context.TodoItem.FirstOrDefault(m => m.Id == id);
        }

        // POST: api/TodoItem
        [HttpPost]
        public void Post(TodoItem todoItem)
        {
            _context.TodoItem.Add(todoItem);
            _context.SaveChanges();
        }

        [HttpPut("{id}")]
        public IActionResult Put(int id, TodoItem todoItem)
        {
            if (id != todoItem.Id)
            {
                return BadRequest();
            }
            _context.TodoItem.Update(todoItem);
            _context.SaveChanges();
            return Ok("ok");
        }

        // DELETE: api/ApiWithActions/5
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            TodoItem todoItem = _context.TodoItem.Find(id);
            if (todoItem == null)
            {
                return NotFound();
            }
            _context.TodoItem.Remove(todoItem);
            _context.SaveChanges();
            return Ok("ok");
        }
    }
}

使用PostMan测试API接口

MVC可以直接输入控制器测试,反正有视图可以看,但是WebAPI这样的需要使用PostMan进行测试

我暂时是直接运行着项目测试的,稍后再讲部署的问题

Get

Post

这个稍微讲一下,参数是json格式的,所以必须选择json,默认是Text格式的,不改成json就会失败

还有一个地方就是禁止Postman的SSL证书,否则也会失败

禁用SSL证书如下

Put

Delete

EF Core根据数据库表生成Model

这个真的是太好用了,把你的项目设置为启动项目,然后 工具>NuGet包管理器>程序包管理器控制台

在程序包管理器控制台输入以下

Scaffold-DbContext "Server=192.168.111.111;Database=VaeDB;uid=sa;pwd=123456789;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

这个很简单可以理解,你的数据库连接字符串,最后是导出的位置是Models,也就是你这个启动项目的Models文件夹下

运行一下,Models就直接生成了

安全

防止XSS (js恶意注入)

XSS是跨站脚本攻击,就是在输入框内输入一些JavaScript代码,从而获取你的cookie之类的信息,也就是js恶意注入,所以我们可以使用HtmlEncoder进行处理,这样<>符号就会变成><这样的,就实现了防止XSS攻击的目的

public string Index(string name, int age=17)
{
    return HtmlEncoder.Default.Encode($"Hello {name},you age is {age}");
}

使用HtmlEncoder.Default.Encode可以实现,当然在.net Core中也可以使用注入的方式

private readonly HtmlEncoder _htmlEncoder;
public MoviesController(HtmlEncoder htmlEncoder)
{
   _htmlEncoder = htmlEncoder;
}
string title = _htmlEncoder.Encode(movie.Title);

使用哪个都可以,但是我的中文也被Encode处理了

防止CSRF攻击

XSS跨站脚本攻击以及知道是在表单之类的输入框输入js代码,那么CSRF又是什么?CSRF是跨站请求伪造,简单的说,就是我有一个表单,我可以添加一个人员,然后直接在浏览器输入https://xxx.movies/Add?Title=123这样也可以添加内容,这个明显是不安全的,我只要求,只能在我的浏览器上使用表单添加信息,换一个浏览器只要不是登录了使用表单,就不允许加入信息,在.net Core中如此配置
在ConfigureServices方法中加入以下代码

//防止CSRF攻击
services.AddAntiforgery(options =>
{
    //使用cookiebuilder属性设置cookie属性。
    options.FormFieldName = "AntiforgeryKey_shuyunquan";
    options.HeaderName = "X-CSRF-TOKEN-shuyunquan";
    options.SuppressXFrameOptionsHeader = false;
});
services.AddMvc(options =>
{
    //这个是给所有的post Action都开启了防止CSRF攻击
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

这样,你的添加方法就已经防止了CSRF攻击,可以在你的表单查看元素,里面会多出一个input标签,name就是你的AntiforgeryKey的名称,就是这个东西才防止了CSRF
注意!!!你的添加方法这些,一定要加上HTTPPost,否则无效

[HttpPost]
public ActionResult AddItem(Movie movie)
{...}

发布

Windows

控制台直接运行

点击你的项目发布,然后你在发布的文件夹内可以看到一个项目名.dll,直接右键打开控制台输入

dotnet Web.dll --urls=http://localhost:8099

当然也可以不指定访问端口号,不写 --urls后面的即可

IIS部署

Swagger的使用

首先要知道Swagger是什么,学了有什么用

Swagger是什么?

就是一个自动生成API文档的框架

Swagger学了能干嘛?

你编写了一个API的后端程序,需要给其他人调用,总得写个文档吧,不然别人怎么知道有哪些API接口,分别是干嘛的

你当然可以选择自己一个一个的写,也可以选择使用Swagger自动生成然后喝杯茶

如何使用Swagger

引入Nuget包

使用Nuget引入Swagger的包,就是这个:Swashbuckle.AspNetCore

在Startup类里配置

Startup里面有两个地方需要配置,首先是需要注入一下服务,然后在中间件那里启用一下

首先,服务注入,在ConfigureServices方法添加Swagger

//Swagger
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { 
        //只列举了几个,还有很多没写,详细可查官方文档
        Title = "My API",
        Version = "v1", 
        Description="我的API的说明文档"
    });
});

然后在中间件的方法Configure中,启用Swagger

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

查看Swagger效果

运行你的Web API项目,然后路由输入swagger,即可

以上简简单单的导入包,注入服务,启用中间件就可以看到简单的文档了,如图

一目了然,接口,还可以测试接口,像postman一样

Swagger进阶

上面简单的实现了Swagger,现在来进阶一下,增加几个功能

  1. 运行API项目后首页直接就是Swagger页面
  2. 加一个接口的描述
  3. 加一个接口的权限验证

首页即是Swagger

先解决第一个问题,运行项目之后首页直接就是Swagger,这个要在launchSettings.json里面设定,在Properties下找到launchSettings.json,修改launchUrl

有两个地方,一个是profiles下的launchUrl,还有一个是项目下的launchUrl

"launchUrl": "swagger",

接口描述

第二个问题,就是API注释,找到你的项目,右键属性,来到生成这里,勾选下面的输出XML文档文件,我这里是项目的根目录,位置自己随便选,然后选定之后重新编译一下项目即可生成xml文件,此时,你会发现出现了非常多的警告,可以在上面的错误和警告那里加一个 ;1591 这样就不会有那么多警告了

在你的API接口上加上注释,如

/// <summary>
/// TodoItem根据Id获取
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}", Name = "Get")]
public TodoItem Get(int id)
{
    return _context.TodoItem.FirstOrDefault(m => m.Id == id);
}

这样运行项目就可以有注释了,如图

虽然新建了一个xml,但是里面什么我们都没加,你现在可以点进去看看,发现xml里面多了好多内容,就是我们刚才写的接口的注释

如果某些接口不希望别人看到的话,可以这样写

/// <summary>
/// TodoItem获取所有
/// </summary>
/// <returns></returns>
[HttpGet]
[ApiExplorerSettings(IgnoreApi = true)]
public IEnumerable<TodoItem> Get()
{
    return _context.TodoItem.ToList();
}

加一个[ApiExplorerSettings(IgnoreApi = true)]就可以了

接口权限

第三个问题权限

下载资源位置和名称的问题

一些静态资源或者上传下载的资源要存放在wwwroot里面

例如我提供一个下载的功能,a标签要写路径必须是/files开始,download属性可以定义下载的文件的名称

<a href="/files/uploads/document/@document.Url" download="@document.Name">@ResourceFile.Download</a>

URL全部转换为小写

许多网站的url都是小写的,所以我们的项目最好全部采用小写url,在Startup里面的

ConfigureServices方法中加上

//url全部转换成小写
services.AddRouting(options => {
    options.LowercaseUrls = true;
});

多个单词使用-分隔

像Privacy这种,直接就显示privacy,这种一个单词的很好,但是我现在有一个方法是AboutUs,这个时候转换小写之后显示的是aboutus,很明显这种不直观,变成about-us会更直观一些,可以在方法上加上

[Route("about-us")]
public IActionResult AboutUs()
{
    return View();
}

如此即可,但是如果有很多方法都需要改的话可以使用 IOutboundParameterTransformer这个接口,但是我没有实现成功,暂时跳过

IOC使用Autofac

虽然.net core有默认的DI,但是默认的那个只能一个一个的添加注入,如果是上百个需要依赖注入的,不敢想,所以,我们可以使用Autofac替代默认的注入,在.net Core2.x版本中,想使用Autofac还需要建立一个类,但是在.net Core3.x版本中,使用Autofac特别简单,只需要2步

  1. 修改Program.cs

CreateHostBuilder这个方法加一个UseServiceProviderFactory

.UseServiceProviderFactory(new AutofacServiceProviderFactory());
  1. 修改Startup.cs

添加一个方法,如下

/// <summary>
/// Autofac依赖注入
/// </summary>
/// <param name="builder"></param>
public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<MovieService>().As<IMovieService>().InstancePerLifetimeScope();
    builder.RegisterType<MovieRepository>().As<IMovieRepository>().InstancePerLifetimeScope();
}

使用xUnit测试项目

先来看看一个最简单的例子,首先我有一个项目叫Web,然后再新建一个xUnit的测试项目,引用Web

Web项目里面有一个类,如下

namespace Web.Models
{
    //此类只为了xUnit的测试用例
    public class Calculator
    {
        public int Add(int x,int y)
        {
            return x + y;
        }
    }
}

xUnit测试项目起名叫WebTest,新建一个类叫CalculatorTest如下

using Web.Models;
using Xunit;

namespace WebTest
{
    public class CalculatorTest
    {
        [Fact]
        public void ShouldAdd()
        {
            //xUnit测试分为3步
            //1.Arrange 做先决条件,例如创建对象实例,数据输入等
            var sut = new Calculator(); //sut是 System Under Test
            //2.Act 执行测试代码并返回结果
            var result = sut.Add(1, 2); 
            //3.Assert 检查结果,测试成功或者失败
            Assert.Equal(3, result);
        }
    }
}

然后直接在ShouldAdd方法上右键执行测试即可.

下面是xUnit的介绍

Assert

断言,这次新建一个类举一些例子,都是很基础的东西

在Web项目中新建一个类,如下

using System;

namespace Web.Models
{
    //病人类,仅作为xUnit测试用例
    public class Patient
    {
        public Patient()
        {
            //病人初始化,雷军发问为Ture
            AreYouOK = true;
        }

        /// <summary>
        /// FirstName
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// LastName
        /// </summary>
        public string LastName { get; set; }

        /// <summary>
        /// 全名,这种=>写法是Lambda表达式,意思是只读,也就是FullName只有get,无法set赋值
        /// </summary>
        public string FullName => $"{FirstName} {LastName}";

        /// <summary>
        /// 雷军发问
        /// </summary>
        public bool AreYouOK { get; set; }

        /// <summary>
        /// 心率
        /// </summary>
        public int HeartBeatRate { get; set; }

        /// <summary>
        /// 增加心率
        /// </summary>
        public void IncreaseHeartBeatRate()
        {
            HeartBeatRate = CalculateHeartBeatRate() + 2;
        }

        /// <summary>
        /// 计算心率,随机返回一个数字即可
        /// </summary>
        /// <returns></returns>
        public int CalculateHeartBeatRate()
        {
            var random = new Random();
            return random.Next(1, 100);
        }
    }
}

在WebTest项目里新建一个测试类如下

using Web.Models;
using Xunit;

namespace WebTest
{
    public class PatientTest
    {
        [Fact]
        public void ShouldBeOkWhereCreate()
        {
            //老样子,xUnit测试三步走
            //1.Arrange 做先决条件,例如创建对象实例,数据输入等
            var sut = new Patient(); //sut是 System Under Test
            //2.Act 执行测试代码并返回结果
            var result = sut.AreYouOK;
            //3.Assert 检查结果,测试成功或者失败
            Assert.True(result);
        }

        [Fact]
        public void HaveCorrectFullName()
        {
            var sut = new Patient
            {
                FirstName = "许嵩",
                LastName = "Vae"
            };
            var fullName = sut.FullName;
            Assert.Equal("许嵩 Vae", fullName); //等
            Assert.StartsWith("许嵩", fullName);//开始包含
            Assert.EndsWith("Vae", fullName);//结尾包含
            Assert.Contains("许嵩 Vae", fullName);//包含
            Assert.Contains("嵩 V", fullName);//包含
            Assert.NotEqual("蜀云泉", fullName);//不等
        }

        [Fact]
        public void HaveIllHistory()
        {
            //针对集合的Assert测试
            var sut = new Patient();
            sut.History.Add("感冒");
            sut.History.Add("咳嗽");
            sut.History.Add("腹泻");
            Assert.Contains("感冒", sut.History);
            Assert.DoesNotContain("心脏病", sut.History);
            Assert.Contains(sut.History, x => x.StartsWith("感")); //集合中包含至少一个是以 感 开头的
        }

        [Fact]
        public void BePatient()
        {
            var sut = new Patient();
            Assert.IsType<Patient>(sut);
        }

        [Fact]
        public void ShouldCalculateHeartBeatRate()
        {
            var sut = new Patient();
            var result = sut.CalculateHeartBeatRate();
            Assert.InRange<int>(result, 1, 100);
        }
    }
}

特征

在测试类或者方法上加上特性Trait即可,如下

[Fact]
[Trait("Category","New")]
public void ShouldBeOkWhereCreate()
{
    //老样子,xUnit测试三步走
    //1.Arrange 做先决条件,例如创建对象实例,数据输入等
    var sut = new Patient(); //sut是 System Under Test
    //2.Act 执行测试代码并返回结果
    var result = sut.AreYouOK;
    //3.Assert 检查结果,测试成功或者失败
    Assert.True(result);
}

然后,在测试窗口选择展示依据为特征

然后在测试窗口就可以单独的选择特征来执行了

还可以忽略测试,只需要给Fact加上Skip即可,如下

[Fact(Skip = "忽略心率测试")]
public void ShouldCalculateHeartBeatRate()
{
    var sut = new Patient();
    var result = sut.CalculateHeartBeatRate();
    Assert.InRange<int>(result, 1, 100);
}

自定义测试输出

使用的是ITestOutputHelper,在PatientTest这个测试类里面新加一个构造函数,使用注入的方式,如下

private readonly ITestOutputHelper _output;

public PatientTest(ITestOutputHelper output)
{
    _output = output;
}

然后使用的时候很简单

[Fact]
[Trait("Category","New")]
public void ShouldBeOkWhereCreate()
{
    _output.WriteLine("自定义测试输出,雷军发问");
    //老样子,xUnit测试三步走
    //1.Arrange 做先决条件,例如创建对象实例,数据输入等
    var sut = new Patient(); //sut是 System Under Test
    //2.Act 执行测试代码并返回结果
    var result = sut.AreYouOK;
    //3.Assert 检查结果,测试成功或者失败
    Assert.True(result);
}

数据驱动测试

回到最初的问题,我们测试计算器这个类的时候,测试类的方法是这样写的

[Fact]
public void ShouldAdd()
{
    //xUnit测试分为3步
    //1.Arrange 做先决条件,例如创建对象实例,数据输入等
    var sut = new Calculator(); //sut是 System Under Test
    //2.Act 执行测试代码并返回结果
    var result = sut.Add(1, 2); 
    //3.Assert 检查结果,测试成功或者失败
    Assert.Equal(3, result);
}

我测试了1+2,结果为3,现在我想多测试几组,那么我只能多写几个方法了,例如2+3,6+3.这样仅仅改变了测试的数据,就要重复的赋值方法不好,所以,引出了数据驱动测试,我们如果仅仅想要多一些测试数据的话,可以这样写

//数据驱动测试,Theory这种特性可以多几组测试数据
[Theory]
[InlineData(1,1,2)]
[InlineData(5,5,10)]
[InlineData(6,2,8)]
public void ShouldAddMany(int x,int y,int expected)
{
    //xUnit测试分为3步
    //1.Arrange 做先决条件,例如创建对象实例,数据输入等
    var sut = new Calculator(); //sut是 System Under Test
    //2.Act 执行测试代码并返回结果
    var result = sut.Add(x, y);
    //3.Assert 检查结果,测试成功或者失败
    Assert.Equal(expected, result);
}

还可以使用外部数据,例如数据库的数据,Excel,Csv等,只需要把InlineData换成MemberData,我这里不写

使用SeriLog记录日志

.net Core自带的Log可以使用,不过这里使用一个第三方日志Serilog

首先在NuGet安装三个包,如下

Serilog.AspNetCore

Serilog.Sinks.Console

Serilog.Sinks.File

修改Program.cs

在Main方法里面加上

//配置Serilog日志
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File(Path.Combine("logs","log.txt"), rollingInterval: RollingInterval.Day)
    .CreateLogger();

在CreateHostBuilder方法的ConfigureWebHostDefaults之前加上

.UseSerilog() //使用Serilog日志

使用Serilog

以HomeController为例,首先注入

private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
    _logger = logger;
}

然后使用,变量可以使用如下方式,不要使用$语法糖拼接变量,要使用下面的变量占位方式

_logger.LogInformation("正在访问首页 {0},{1}",1,2);
_logger.LogWarning("这是一个严重的警告日志,错误变量{0},{1}",555,666);

因为我们在Main方法里面的设置,日志会输出到控制台和保存到我们指定的logs文件夹下面,会生成一个log的txt文件

现在还需要解决一个问题,就是我希望Infomation的日志保存在一个地方,Warn的日志单独保存一个地方,这样好找,暂留这个问题

使用MemoryCache缓存

这个MemoryCache缓存是自带的缓存

Staup开启缓存

在ConfigureServices方法中添加开启缓存

//使用缓存
services.AddMemoryCache();

新建缓存key值类

新建一个类,里面放的全是缓存的key值,没有key你怎么读取存储缓存,如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Web.Data
{
    /// <summary>
    /// 存放缓存的Key
    /// </summary>
    public class CacheEntryConstants
    {
        /// <summary>
        /// HomeController里面的TestMemoryCache
        /// </summary>
        public const string TestMemoryCache = nameof(TestMemoryCache);

        /// <summary>
        /// MovieController里面的获取MovieList的Index方法
        /// </summary>
        public const string MovieList = nameof(MovieList);
         
    }
}

使用缓存

使用缓存之前应该先从缓存中读取,没有的话从数据读取,然后赋值给缓存

private readonly IMemoryCache _memoryCache;
private readonly IMovieService _movieService;

public MoviesController(
    IMovieService movieService,
    IMemoryCache memoryCache)
{
    _movieService = movieService;
    _memoryCache = memoryCache;
}

public async Task<IActionResult> Index()
{
    if (!_memoryCache.TryGetValue(CacheEntryConstants.MovieList,out List<Movie> cacheMovieList))
    {
        cacheMovieList = await _movieService.Query();
        var cacheEntryOptions = new MemoryCacheEntryOptions()
           //.SetAbsoluteExpiration(TimeSpan.FromSeconds(600))强制600s之后缓存失效
           .SetSlidingExpiration(TimeSpan.FromSeconds(30));//动态设定缓存,访问就+30s,没人访问就30s之后缓存失效
        //新设置缓存,key,值,参数
        _memoryCache.Set(CacheEntryConstants.MovieList, cacheMovieList, cacheEntryOptions);
    }
    return View(cacheMovieList);
}

那个测试使用的Test我也给放出来,很简单的

private readonly IMemoryCache _cache;

public HomeController(ILogger<HomeController> logger, IMemoryCache cache)
{
    _cache = cache;
}
public string TestMemoryCache()
{
    //首先判断缓存中是否有数据了
    if (!_cache.TryGetValue(CacheEntryConstants.TestMemoryCache,out string cacheTestMemoryCache))
    {
        //如果缓存中没有,则赋值,且加入缓存
        cacheTestMemoryCache = "测试缓存";
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            //.SetAbsoluteExpiration(TimeSpan.FromSeconds(600))强制600s之后缓存失效
            .SetSlidingExpiration(TimeSpan.FromSeconds(30));//动态设定缓存,访问就+30s,没人访问就30s之后缓存失效
        //新设置缓存,key,值,参数
        _cache.Set(CacheEntryConstants.TestMemoryCache, cacheTestMemoryCache, cacheEntryOptions);
    }
    return cacheTestMemoryCache;
}
posted @ 2019-07-17 01:30  蜀云泉  阅读(2727)  评论(0编辑  收藏  举报