.Net Core个人笔记
- 前言
- IOC注册
- .Net Core部署IIS之后500错误
- 管道和中间件
- 使用MVC
- 三种环境配置
- 使用HTTPS
- EntityFrameWork Core的入门使用
- Build failed.
- 严重性代码说明项目文件行禁止显示状态错误 MSB3541 Files 的值“<<<<<<< HEAD”无效。路径中具有非法字符。 StudyNetCore D:\Programs\VisualStudio2019\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets
- Unable to create an object of type 'MyContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
- 开始EF Core入门
- 第一步,新建几个Model
- 第二步,新建EF Core迁移
- 第三步,简易的增删改查
- EF Core的数据库连接字符串写在配置文件中
- EntityModel和ViewModel的转化使用 (简单的推荐使用,复杂的推荐使用AutoMapper)
- 使用AutoMapper (推荐)
- 输入Model和防止重复post
- Model数据验证
- View
- Identity:身份验证
- 使用TagHelper
- 字段重命名DisplayNameFor和Display
- WebAPI
- EF Core根据数据库表生成Model
- 安全
- 发布
- Swagger的使用
- 下载资源位置和名称的问题
- URL全部转换为小写
- IOC使用Autofac
- 使用xUnit测试项目
- 使用SeriLog记录日志
- 使用MemoryCache缓存
前言
学习Net Core的个人笔记,记录
建议看微软官方文档,看不懂的查一下 教程:ASP.NET Core 入门
IOC注册
Startup类中的ConfigureServices方法是用于服务注册IOC
ConfigureServices这个方法是用于服务注册的,服务就是IOC里面的类
三种生命周期
IOC容器内的服务有三种生命周期
- Transient:每次请求都会创建一个新的实例
- Scoped:每次Web请求都会创建一个实例
- 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,如下
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- 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
- EntityModel:也称之为实体Model,这种Model的字段和数据库表的字段是一一对应的
- 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; }
}
Required
和MinimumLength
特性表示属性必须有值;但用户可输入空格来满足此验证。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,现在来进阶一下,增加几个功能
- 运行API项目后首页直接就是Swagger页面
- 加一个接口的描述
- 加一个接口的权限验证
首页即是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步
- 修改Program.cs
CreateHostBuilder这个方法加一个UseServiceProviderFactory
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
- 修改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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!