ASP. NET Core基础框架(MVC)

tips:

  1. ASP. NET Core 有两种不同的基础架构,分别为MVC结构与Razor Pages结构, 本文只探讨应用更广泛的MVC结构,但是读者应当了解Razor Pages结构在小型简单网站上具有一定的优越性。Razor Pages资料参考MSDOCS

  2. 本系列教程基于Asp. Net Core 3.1版本,下文中缩写为ASP

  3. 前提条件:阅读者应当具有基础C#编程知识,或较为熟练的C/C++编程技能

Starting

首先,我们需要创建一个MVC项目
请参考MSDOCS
在继续阅读本教程前,请按照上方链接里的步骤一步一步执行,直到成功创建并运行基础MVC。

Staging

你按照上方链接完成了吗?很好,此时你应当已经获得了一个基础的ASP. NET Core MVC应用。
此时,打开你的解决方案资源管理器,你的项目结构应该像这样:
STRUCT
你明白为何这个结构叫MVC了吗?
MVC,是Model-View-Controller 的缩写,也就是模型-视图-控制器

模型(Model)

所谓模型,就是各种数据结构的总称。不同的是,相比C/C++中经常用来写结构的struct,c#中一般使用class。

tips: 这并不代表c#中没有struct,正相反c#中也有struct这一数据类型。在c#中struct在栈中分配空间,而class在堆中分配空间。所以使用struct实际上在某些时候比使用class更快。但是class可以干很多struct不能做的事。参考struct/class

注意,c#的中的模型并不一定只包含需要储存的数据,他也可以包含一些处理方法(c中的函数),例如:

public class FoodModel
{
    public Guid FoodId { get; set; }
    public float Price { get; set; }
    public float AddPrice(FoodModel food1, FoodModel food2)
    {
        return food1.Price + food2.Price;
    }
}

tips: 上方的代码中使用自动实现的属性,不了解的同学请参考MSDOCS

视图(View)

tips:

  • View在Web Api后端中时不被需要的,因为Web Api应当配套专门设计的SPA前端(或App)。如果仅仅想学习Web Api,应当跳过此部分。

  • 下方的链接里的参考知识比较多,汉化的不是很好,且仅仅是标准的ASP前端的教学。如果不想使用标准ASP前端,可以不用查看参考链接。

  • 即使想学习标准ASP前端,也可以暂时跳过参考链接

  • 标准ASP前端优势:VS包含有为其定制的强大的代码生成器,熟练使用可以极快制作出产品(几乎不用自己写代码)。但其表现力当然比不上SPA
    所谓视图,在ASP一般中就是.cshtml文件的集合。它们将会在接受请求时被动态编译为html文件并发送给浏览器以显示为网页。
    其中涉及到的技术主要有(除基础的html和css、js): 模型绑定tag helper、html helper

控制器(Controller)

控制器对用户请求时上传的信息进行处理,并返回对应的数据(WEB API)或视图。
API Controller类一般继承ControllerBase,而标准ASP的Controller一般继承Controller
Controller本身继承ControllerBase
参考MSDOCS

StartUp

仔细看程序的Program类,你会发现其实ASP本身是一个控制台程序,它启动时用StartUp类作为设置

Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });

所以一个好的ASP使用者必须了解StartUp的工作原理。

首先,StartUp类中有一个接收一个接口IConfiguration类型参数的构造函数。
之所以接收这个参数和依赖注入有关,我们现在先放着不管。实际上,在默认模板中,你是可以删去此参数的(把里边的赋值操作一起删除掉)。
然后第一个方法应该和下边一样:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
}

这个方法本身用来自定义依赖注入。

依赖注入

依赖注入是一项很重要的技术,大家应该熟练掌握。

不知道大家有没有为变量的生命周期而烦恼过?我估计没有,因为学校的作业从来不管学生的优化,只管结果。
但是在生产中,生命周期管理是很重要的。
我们来设想一个场景:我们有一个数据库查询的类,它

  • 每次实例化需要分配100mb的空间

  • 实例化从开始到完成(与数据库建立稳定连接)要5s

  • 每次GC销毁它需要挂起其它线程0.2秒(100mb需要动用2代GC)

  • 它是线程安全的(可以同时被很多线程共享)

那么,我们应该每次接收请求都产生一个新的查询类还是在第一次请求时生成一个查询类,然后之后所有请求用一个查询类呢?
很明显,对于消耗大,线程安全的类应当选择后者。
那么为了这样做,我们应该怎么做?在Program类中声明一个静态的查询类参数,然后每次使用时调用它?也就是说每次我使用的时候:

Program.Query.XXX(...);

不觉得很麻烦吗?
而且这样,那些消耗小、线程不安全的类,就需要每次在使用时初始化,即

var transient = new Transient();

仅仅这样也许看不出有多麻烦,那么万一Transient的构造函数必须接受十个参数呢?

var transient = new Transient(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);

你还觉得不麻烦吗?每次写一遍其实不仅很麻烦,还容易错。
于是,有人想出了一个完美的解决方案,它叫:依赖注入(Dependency Injection)
依赖注入只需要你定义一次实例化方法它就会全自动地帮你管理各种实例地生命周期,在你要使用时,给需要使用的函数添加对应类型地参数即可。
依赖注入有个容器用于存储所有的实例。这个容器在这儿就是IServiceCollection
在asp中,程序会自动帮你注入几个服务,比如IHostEnvironment
ConfigureServices方法中,我们可以通过传入的参数services注入我们需要的服务。
很多服务官方已经帮你封装,比如

services.AddControllersWithViews();

这个方法帮你注入基础的MVC服务。
如何注入自定义服务呢?有三种快速注入方法:

  1. services.AddSingleton<T>(),T为泛型(即实际使用时替换成你需要的类型),这个方法会把T注入为单例,也就是无论多少请求,共用一个实例。

  2. services.AddTransient<T>(),T为泛型(即实际使用时替换成你需要的类型),这个方法会把T注入为多例,T将会在每次被注入到函数时生成一个新实例。

  3. services.AddScoped<T>(),T为泛型(即实际使用时替换成你需要的类型),这个方法会把T注入为一个范围内地单例,这个范围是可以自定义地,一般不定义的话默认时每个用户一个实例。

更多细节,参考MSDOCS

HTTP 管道

在ASP中,每个请求都被抽象为管道,我们编程时可以设计一条流水线对其进行处理。
流水线的设计处就是StartUp类里的Configure方法。

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

以上时自动生成地模板中的Configure方法。里边的每个app.UseXXXX()都是一个中间件,也就是流水线上的一台处理机器,处理完的结果会交给下一个app.UseXXXX()进行处理。上方代码的最后一步是

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

正是这一步把上边经过多重处理后的请求转发给我们写的controller类进行处理,然后返回View
我们如何自定义中间件呢?
很简单,用app.Use()方法就行:

app.Use(async (context, next) =>
{
    var cultureQuery = context.Request.Query["culture"];
    if (!string.IsNullOrWhiteSpace(cultureQuery))
    {
        var culture = new CultureInfo(cultureQuery);

        CultureInfo.CurrentCulture = culture;
        CultureInfo.CurrentUICulture = culture;
    }
    await next();
});

以上函数增加了一个中间件:它找出url中的culture信息,然后设定请求者的culture以便之后的中间件使用。
最后一步 await next()的作用是调用管道中下一个中间件进行处理。而接收函数中context是HttpContext类型,包含了请求和回复(如果有的话)的所有信息。
更多信息见MSDOCS

以上就是ASP程序的基础结构。

参考资料:
MSDN ASP.NET Core 文档