键盘敲烂,月薪过万作业不做,等于没学
当前系列: ASP.NET 修改讲义
说明:学习重点core和Framework的区别。

项目和文件结构


  1. 类控制台风格:入口Program.Main()
  2. 应用配置主要在:Startup.cs
  3. 配置文件使用json格式
    • appSettings.json
    • launchSettings.json

在VS中ASP.NET core项目有两种启动方式:(launchSettings.json中设置commandName)

  1. IIS Express:同Framework
  2. Project:只有一个控制台/黑窗体:
PS:发布更新/更改appSettings后需要重启IIS website


route

可能是为了避免混淆,最新版的ASP.NET core WebApi不支持使用Conventional的route配置模式。

所以只能使用特性。

[ApiController]

core中不是让Controller继承自ApiController,而是要在其上声明[ApiController],表明这个Controller可以作为Route的Endpoint使用。

一旦声明了[ApiController](也可以不声明,但WebApi中推荐,其作用详见:微软文档),就必须要声明[Route]特性:

演示:略
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

[controller]代指当前的Controller名称,此处就是WeatherForecast。

其他和Framework无异。(演示:略)


Controller相关

所有controller继承的是:

ControllerBase

ASP.NET WebApi的Controller继承的是ControllerBase

在core中就彻底废弃了HttpContext.Current,但Request和Response相比WebApi FW就更人性化一些,更贴近ASP.NET的传统(MVC)一些。

演示:查看属性

  • Request
  • Response
  • HttpContext
  • RouteData


Action相关

一样可以:

  • Model绑定/验证
  • [FromBody]等修饰参数
  • 通过Ok()/BadRequest()/Content()等返回IActionResult(是IHttpActionResult)对象

ActionResult<T>

注意Content()方法只能传入string参数(再是泛型类型),所以如果要同时能返回IActionResult和自定义类型对象,咋办?

public /*???*/ Get(int id)
{
    var rng = new Random();
    if (rng.Next(100) % 2 == 0)
    {
        return BadRequest();
    }
    else
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast

这就需要ActionResult<TValue>了:

public ActionResult<WeatherForecast[]> Get(int id)

转到定义:

public sealed class ActionResult<TValue> : IConvertToActionResult
@想一想@:这怎么实现的呢?复习:类型转换重载

JSON vs XML

默认WebApi core返回的是JSON格式的数据。

如果需要返回其他格式,比如XML,

首先需要在Startup.cs的ConfigureServices()中添加一个能够将C#对象转换成XML格式的service:

services.AddControllers().AddXmlSerializerFormatters();
然后,
  • 如果前端有要求(需要使用Postman设置Accept),WebApi自动按请求生成XML
  • 如果要服务器端(不管前端什么要求)自行生成并返回XML格式,需要
    在Controller或Action上添加:
    [Produces("application/xml")]
    或者,如果要作用于整个项目的话,就在ConfigureServices()中:
        services.AddMvcCore(opt =>
        {
            opt.Filters.Add(new ProducesAttribute("application/xml"));
        })


Service

core内置的依赖注入工具。(复习:Autofac

由依赖注入机制生成的对象,在ASP.NET中被称为service

注册/声明

需要在Startup.cs的ConfigureServices()中添加:

services.AddSingleton<IUserService, UserService>();

这个过程被称之为:注册(演示:还有其他重载方法)

它告诉ASP.NET core项目:在使用依赖注入时,用UserService对象作为IUserService变量(或者参数)的引用。或者说,当需要一个IUserService变量时,给一个UserService对象。

层层注入

我们需要一个UserController的有参构造函数:

public class UserController : ControllerBase
{
    private IUserService service;
    public UserController(IUserService service)
    {
        this.service = service;

而且在Service中的一样可以注入Repository:

public class UserService : IUserService
{
    private IUserRepository userRepository;
    public UserService(IUserRepository userRepository)
    {
        this.userRepository = userRepository;

这里的IUserRepository究竟使用哪个实例对象,一样需要注册/声明:

services.AddSingleton<IUserRepository, UserRepository>();

.NET core transitive reference

观察:因为SRV引用了BLL(repository),所以UI只要引用了SRV项目,就不需要再显式的添加对BLL层项目的引用……

这是.NET core 1.1开始引入的新特性

主要是为了解决在startup.cs中注册所有service的问题,从而严重的破坏了传统“分层架构”不能跨层调用的原则。

反过来促使我们思考:严格的分层是否有必要?

-- 感慨:技术的“螺旋式进步,破浪式前进

蔓延/切断

类似于async方法的蔓延:只要底层进行了注入,上层也得注入,如上示例。

#试一试#:假设我们只需要Repository对Service的依赖注入,不需要在Controller中注入Service,行不行?

可以直接new一个repository对象传入:

this.service = new UserService(new UserRepository());
但这样就丧失了依赖注入(在startup.cs中设置)的灵活性了。(演示)

正确的做法是在UserController中:

private IUserService service;
public UserController(IUserRepository repository)
{
    this.service = new UserService(repository);
}

通过构造函数注入!

service的生命周期

AddXXX()后面的XXX:

  • Transient:用一次就创建一个
  • Scoped:一次Request中只创建一个
  • Singleton:整个Application都只创建一个

准备额外的

public class ArticleService : IArticleService
{
    private IUserRepository userRepository;
    public ArticleService(IUserRepository userRepository)
    {
        this.userRepository = userRepository;
让ArticleService和UserService都依赖IUserRepository。

断点演示:所有的Service使用

  • 相同的生命周期:
    • Singleton时:F5启动一次,无论多少次请求,只击中一次构造函数
    • Transient时:一次请求中,ArticleService和UserService构造函数中传入的IUserRepository对象有不同的地址(watch*/&)
    • Scope时:同一次请求,同一个IUserRepository对象;不同的请求,不同的IUserRepository对象
  • 不同的生命周期:
    • Singleton的Service,和Transient/Scope的Repository:singleton的Repository……
    • Transient的Service,和Singleton/Scope的Repository:非Transient的Repository……
    • Transient/Scope/Singleton混合,就容易出篓子报异常了,^_^……

Service的应用

ASP.NET core和Framework最大的不同(难点),就在于core项目中大量有意无意的Service(依赖注入)应用。

IHttpContextAccessor

比如我们要在SRV层中获得cookie,就首先要获得HttpContext对象,.NET Framework类库项目可以引入System.Web.dll,通过HttpContext.Current获得。

但.NET core的类库项目,是做不到的。(演示)

只能通过依赖注入:

public class UserService : IUserService
{
    private HttpContext context;
    public UserService(IHttpContextAccessor accessor)
    {
        this.context = accessor.HttpContext;
IHttpContextAccessor首先需要在Startup.cs中注册
services.AddHttpContextAccessor();

PS:AddHttpContextAccessor()实际上也不过是封装了AddScoped()……

微软推荐:使用扩展方法Add{ServiceName}合并service注册。

获取配置信息

比如连接字符串,写在appsettings.json文件中:

  "ConnectionStrings": {
    "_17bang": "Data Source=(localdb)\\ProjectModels;Initial Catalog=17bang;Integrated Security=True;"
  }

需要通过IConfiguration对象获取。

如果是在startup.cs中,该对象已经通过属性注入:

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

然后就可以直接使用:

string connStr = Configuration.GetConnectionString("_17bang");

如果不介意在UI层直接引入EF的话,可以直接使用ASP.NET core的便捷方法AddDbContext()

services.AddDbContext<SqlDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("_17bang"));
或者,其他项目可以“依葫芦画瓢”,比如在ProdRepository类库项目中:

然后在SqlDbContext中注入:

public class SqlDbContext : DbContext
{
    private IConfiguration _configuration;
    public SqlDbContext(IConfiguration configuration)
    {
        this._configuration = configuration;

但是,这也导致必须在调用SqlDbContext的地方注入依赖(依赖蔓延):

public class UserRepository : IUserRepository
{
    private IConfiguration configuration;
    public UserRepository(IConfiguration configuration)
    {
        this.configuration = configuration;

最后,如果是其他项目(比如DbFactory)引用SqlDbContext的话,就需要:

  1. 在起始项目中添加appsettings.json文件,利用该配置文件
  2. 通过ConfigurationBuilder获得(build())一个IConfiguration对象。SetBasePath()指定路径,AddJsonFile()指定.json文件名: 
    IConfiguration config = new ConfigurationBuilder()
        .SetBasePath(Environment.CurrentDirectory)
        .AddJsonFile("appsettings.json")
        .Build();
    演示:项目根目录下的.json文件编译时会copy到Environment.CurrentDirectory
  3. 将IConfiguration对象传入SqlDbContext:
    SqlDbContext context = new SqlDbContext(config);

注意这里面的一些“大坑”:

  • GetConnectionString()是一个扩展方法,使得自定义一个IConfiguration实现类,实现GetConnectionString()变得不可能
  • SetBasePath()和AddJson()还是(其他程序集的)扩展方法,使得VS的Alt+Enter智能提示变得不可能(只能查文档,nuget添加Microsoft.Extensions.Configuration.Json)
  • 只能使用json配置文件,不能使用xml文件,但Microsoft.Extensions.Configuration.Xml的程序集实现,且可以成功的生成IConfiguration对象,但就是读不出ConnectionString,因为GetConnectionString()是仅针对.json配置的……

#体会#:类库架构API应符合人的惯常预期,别人连蒙带猜就能用起来的类库/架构,才是好类库/架构

ContextPerRequest实现

基于Service生命周期,我们可以非常方便的实现。只需要将之前的:

public User GetByName(string username)
{
    return new SqlDbContext(configuration).Set<User>()

改成:

private DbContext dbContext;
public UserRepository(DbContext dbContext)
{
    this.dbContext = sqlDbContext;
public User GetByName(string username)
{
    return dbContext.Set<User>()
然后,在startup.cs中注册SqlDbContext的时候,就确定他们的生命周期为scoped:
services.AddScoped<DbContext, SqlDbContext>();


中间件

PS:对标ASP.NET Framework的ModuleHandler

ASP.NET core更加的“暴露”,在Startup.cs的Configure()中已有中间件配置:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseHttpsRedirection();
    app.UseRouting();

所以简单的讲一讲。

理解:一个HTTP请求到达ASP.NET之后,会:

  1. 首先在管道(pipeline)中经过一个又一个的中间件(middleware)“”处理(——对应Module
  2. 最后达到一个终点(EndPoinit),生成响应返回(——对应Handler
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();  //Route的终点(Endpoint)为Controller
    });

和Filter类似,但(大部分)在Filter之前,比Filter适用范围更广,(如果自定义实现的话)不受时间节点限制。

我们开发中一般不会自定义的创建中间件,只是使用现成的中间件进行配置。配置时注意其顺序!

演示:

  • 通过(注释)对比说明中间件的作用
  • 使用endpoints.MapRazorPages();替换endpoints.MapControllers();(对比=>融汇贯通)


Cookie

ASP.NET core中对cookie进行了重构:区分了ResponseCookie(生成)和RequsetCookie(获取)!

生成

演示:生成/获取

Response.Cookies.Append("UserId", "986");
注意:
  • 由Response引导,表明这个cookie是存放在Response中的
  • Cookies是复数形式,一个Response中可以存放多个cookie
  • 该cookie的名称(name/key)是UserId,值(value)是986

这样,我们就能在浏览器中看到生成的cookie了(chrome:F12->Application)

我们还可以通过

CookieOptions

自行指定cookie的过期时间、域名、路径、是否“机要”等:

Response.Cookies.Append("userId", "id=18", 
    new CookieOptions
    {
        Expires = System.DateTimeOffset.Now.AddDays(1),
        Domain = ".17bang.ren",
        Path = "/Log",
        IsEssential = true
    });

PS:关于IsEssential:在ASP.NET core 3.1版本之前,为了避免网站利用cookie收集用户隐私,国外已普遍立法要求使用cookie前应经过用户同意。

所以,ASP.NET core通过在startup.cs的Configure()方法中:

app.UseCookiePolicy();
强制要求cookie的使用必须先经过用户同意。但如果在生成某个cookie的时候指定IsEssential=true,就可以绕过这个限制。

获取

使用Try模式:

bool hasCookie = Request.Cookies.TryGetValue("userId", out string userId);


session

session在ASP.NET core项目中的应用要麻烦一些。首先就需要

显式的开启

startup.cs中配置:

  • Configure()中添加:
    app.UseSession();
  • ConfigureServices()中添加:
    services.AddSession();
另外,session的存放在cache(后文详述)中,所以还需要:
services.AddDistributedMemoryCache();

配置

可以在两个地方配置session的过期时间、使用的cookie等:

  • services.AddSession():
    services.AddSession(option =>
    {
        option.Cookie = new CookieBuilder
        {
            Name = "17bang"
  • app.UseSession():
    app.UseSession(new SessionOptions
    {
        IdleTimeout = TimeSpan.FromSeconds(5),

存入

ASP.NET core的session能直接存放(自定义)对象了。

ISession定义的Set()方法

void Set(string key, byte[] value)
参数value类型为byte[](@想一想@:为什么?便于序列化

但这非常不便于开发人员,所以有了存放int和string的扩展方法:SetInt32()和SetString()

读取

可以用ISession的实例方法TryGetValue(),也可以使用其扩展方法:GetInt32()和GetString()
int? userId = HttpContext.Session.GetInt32("userId");
if (userId == null)
{
    HttpContext.Session.SetInt32("userId", 18);

JsonConvert

如果要在session中存放对象的话,需要先将对象序列化。

推荐使用Newtonsoft的JsonConvert:

if (string.IsNullOrEmpty(jsonUser))
{
    jsonUser = JsonConvert.SerializeObject(user);

else
{
    user = JsonConvert.DeserializeObject<Entities.User>(jsonUser);  


文件

上传

需要异步Action,代码如下:

IFormCollection forms = await Request.ReadFormAsync();
IFormFile file = forms.Files[0];

using (var stream = System.IO.File.Create(
    Path.Combine(server.WebRootPath, file.FileName)))
{
    file.CopyTo(stream);
其中,server来自依赖注入:
public StudentController(IWebHostEnvironment server)
{
    this.server = server;
}

Postman演示

下载

可以调用File()返回FileContentResult,同MVC,略。

唯一需要注意的是:因为是.NET core项目,所以需要引入:Assembly System.Drawing.Common


Filter

和MVC混用,Result相关内容在WebApi中自动忽略。

多了一个Resource filtersauthorization之后,Action之前,可

  • 更早的介入,比如处理缓存
  • 控制/影响Model绑定

可以继承/实现异步的Filter:

public class AddHeaderFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        //ActionExecuting
        await next();
        //ActionExecuted

全局注册在Startup.cs的ConfigureServices()中,比如:

services.AddControllers(opt=>
    {
        opt.Filters.Add(typeof(AddHeaderFilter));
    })

依赖注入

如果我们的Filter中需要Service,咋办?如果使用构造函数注入的话:

public class AddHeaderFilter : ActionFilterAttribute,  IAsyncActionFilter
{
    private IUserService userService;
    public AddHeaderFilter(IUserService userService)
    {
        this.userService = userService;

使用AddHeaderFilter的时候,语法都搞不定:

[AddHeaderFilter(/*???*/)]

这时候只有使用:

  • ServiceFilter
    [ServiceFilter(typeof(AddHeaderFilter))]
  • 或TypeFilter
    [TypeFilter(typeof(AddHeaderFilter), 
        Arguments = new object[] { "yz", "17bang" })]

两者的区别:

  • ServiceFilter还需要在ConfigureServices()中注册/声明,
    services.AddControllers(opt =>
        {
            opt.Filters.Add(typeof(AddHeaderFilter));
    TypeFilter不需要,因为TypeFilter的实例生成依赖于:Microsoft.Extensions.DependencyInjection.ObjectFactory.
  • TypeFilter可以通过Arguments同时传递构造函数参数,ServiceFilter不行。


缓存

ASP.NET core提供了两个缓存接口,一般通过Service的方式使用:

  • IMemoryCache:数据缓存在内存中
  • IDistributedCache:数据缓存在“分布式”组件中(Redis、MongoDB、DB...)
为演示方便,我们以MemoryCache进行讲解。

IMemoryCache

首先需要在Startup.cs的ConfigureServices()中配置:

services.AddMemoryCache();

然后,在Controller的构造函数中注入

private IMemoryCache cache;
public UserController(IMemoryCache cache)
{
    this.cache = cache;

断点演示:_cache的类型

接下来就可以直接使用_cache对象了,它有两个实例方法:

  • TryGetValue():获取缓存数据
  • Set():存入缓存数据
if (!cache.TryGetValue("time", out DateTime time))
{
    cache.Set("user", DateTime.Now);
另外还有一个扩展方法:
GetOrCreate(),可以一次性的完成上述逻辑:先从cache中获取,获取不到就获取且存放到缓存:
    _cache.GetOrCreate(cacheEntry, c => DateTime.Now);

MemoryCacheEntryOptions

缓存选项”(比如缓存多久)需要由MemoryCacheEntryOptions对象设置:
var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3))
            .SetAbsoluteExpiration(DateTime.Now.AddMinutes(1));

注意:在ASP.NET core中,Absolute和Slide可以combine:

  • 在Absolute时间段里,可以通过Slide的方式过期
  • Slide永远不会超过Absolute

然后,将其传入Set()方法中:

cache.Set(cacheKey, cacheEntry, cacheEntryOptions);

限制和清理

ASP.NET core中会自动清理缓存,所以我们有可能需要自行限制缓存的容量,详见:Use SetSize, Size, and SizeLimit to limit cache size

[ResponseCache]

注意:不要把它和Framework的[OutputCache]相混淆,(*/ω\*)

ResponseCache仅仅是在Response Header中添加条目,本身并不进行缓存:

[ResponseCache(Duration = 50)]

就会生成:

Cache-Control: public, max-age=50

public可以变成:

  • private:Location = ResponseCacheLocation.Client
  • no-cache:NoStore = true

你可以理解为:服务器“告诉/提示”客户端如何缓存数据到本地(及其“中间节点”)。

断点演示:每一次HTTP请求都会击中Action

还可以通过:

VaryByHeader = "User-Agent"

添加一个:

vary: User-Agent

VaryByQueryKeys = new string[] { "name" }

需要开启 Response Caching Middleware中间件。在StartUp.cs中设置:

  • ConfigureServices()方法中
       services.AddResponseCaching();
  • Configure()方法中
       app.UseResponseCaching();


作业

同:WebApi(FW)

学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码