基於ASP.NET core的MVC站點開發筆記 0x01

CN_Simo發表於2020-07-05

基於ASP.NET core的MVC站點開發筆記 0x01

我的環境

OS type:mac
Software:vscode
Dotnet core version:2.0/3.1

dotnet sdk下載地址:https://dotnet.microsoft.com/download/dotnet-core/2.0

準備

先到上面提供的下載地址,下載對應平臺的dotnet裝上,然後在命令列視窗輸入dotnet --version檢視輸出是否安裝成功。

然後,安裝visual studio code,安裝之後還需要安裝C#擴充,要不然每次開啟cs檔案都會報錯。

建立專案

新建一個空目錄,例如mvc-test

使用命令dotnet new檢視可以新建的專案型別:

第一次嘗試,使用ASP.NET Core Empty就可以,代號是web,使用命令dotnet new web就可以新建一個空專案,專案的名稱就是當前目錄的名字mvc-test

專案結構與預設配置

目錄主要結構和檔案功能如下:

Program.cs是程式的主類,Main函式在這裡定義,內容大致可以這麼理解:

CreateDefaultBuilder函式會使用預設的方法載入配置,例如通過讀取launchSettings.json確定當前的釋出環境:

webhost通過ASPNETCORE_ENVIRONMENT讀取釋出環境,然後會讀取對應的配置檔案,Development對應appsettings.Development.jsonProduction對應appsettings.json

appsettings檔案是整個web應用的配置檔案,如果web應用需要使用某個全域性變數,可以配置到這個檔案裡面去。

webhost在執行前會通過Startup類,進行一些中介軟體的配置和註冊,以及進行客戶端的響應內容設定:

注:dotnet core 3版本里,取消了WebHost,使用Host以更通用的方式進行程式託管。

dotnet core 3 Program.cs

public static Void Main(string[] args)
{
    Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder =>
    {
        builder.UseStartup<Startup>();
    }).Build().Run();
}

獲取配置檔案中的值

修改launingSettings.json中設定的釋出環境對應的配置檔案,例如appsetttings.Delelopment.json內容,新增一個Welcome欄位配置項,如下:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "Welcome": "Hello from appsettings.json!!"
}

修改Startup.cs檔案,新增IConfiguration config引數,.net core內部會將配置檔案內容對映到這個變數:

/// <summary>
/// 註冊應用程式所需的服務
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
}

/// <summary>
/// 註冊管道中介軟體
/// </summary>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IConfiguration config)
{
    // 開發環境,使用開發者異常介面
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    var welcome = config["Welcome"];

    // Run一般放在管道末尾,執行完畢之後直接終止請求,所以在其後註冊的中介軟體,將不會被執行
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync(welcome);
    });
}

在終端中使用命令dotnet run可以執行這個web應用:

瀏覽器訪問http://localhost:5000,可以看到已經成功獲取到Welcome配置項的值:

日誌列印

通過ILogger實現控制檯日誌的列印:

public void ConfigureServices(IServiceCollection services)
{
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    var welcome = config["Welcome"];

    logger.LogInformation(welcome);

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync(welcome);
    });
}

ILogger使用的時候需要指定列印日誌的類名Startup,最終列印效果如下:

服務註冊

上面的IConfiguration可以直接使用,是因為IConfiguration服務已經自動註冊過了。

對於自定義的服務,可以在ConfigureServices中註冊,例如自定義一個服務WelcomeService,專案目錄下新建兩個檔案IWelcomeService.csWelcomeService.cs,內容如下:

/* IWelcomeService.cs
 *
 * 該介面類定義了一個getMessage方法。
 */
namespace mvc_test
{
    public interface IWelcomeService
    {
        string getMessage();
    }
}
/* WelcomeService.cs
 *
 * 該類實現了getMessage方法。
 */
 namespace mvc_test
{
    public class WelcomeService : IWelcomeService
    {
        int c = 0;
        public string getMessage()
        {
            c++;
            return "Hello from IWelcomeService Interface!!!" + c.ToString();
        }
    }
}

然後在ConfigureServices中註冊服務:

public void ConfigureServices(IServiceCollection services)
{
        services.AddSingleton<IWelcomeService, WelcomeService>();
}

然後在Configure中使用的時候需要傳參:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger,
    IWelcomeService welcomeService)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    //var welcome = config["Welcome"];
    var welcome = welcomeService.getMessage();

    logger.LogInformation(welcome);

    // Run一般放在管道末尾,執行完畢之後直接終止請求,所以在其後註冊的中介軟體,將不會被執行
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync(welcome);
    });
}

執行後結果:

這個例子中,註冊服務使用的函式是AddSingleton,服務的生命週期除了Singleton,還有其他兩個模式:ScopedTransient

這三個模式的區別:

  • Transient:瞬態模式,服務在每次請求時被建立,它最好被用於輕量級無狀態服務;
  • Scoped:作用域模式,服務在每次請求時被建立,整個請求過程中都貫穿使用這個建立的服務。比如Web頁面的一次請求;
  • Singleton:單例模式,服務在第一次請求時被建立,其後的每次請求都用這個已建立的服務;

參考資料:

初始學習使用AddSingleton就行了。

中介軟體和管道

中介軟體是一種用來處理請求和響應的元件,一個web應用可以有多箇中介軟體,這些中介軟體共同組成一個管道,每次請求訊息進入管道後都會按中介軟體順序處理對應請求資料,然後響應結果原路返回:

參考資料:

內建中介軟體的使用:處理靜態檔案訪問請求

新建一個目錄wwwroot,目錄下新建index.html檔案:

<html>
    <head>
        <title>TEST</title>
    </head>
    <body>
        <h1>Hello from index.html!!!</h1>
    </body>
</html>

使用之前的程式碼,dotnet run執行之後訪問http://localhost:5000/index.html,發現還是之前的結果,並沒有訪問到index.html

這時候需要使用中介軟體StaticFiles來處理靜態檔案的請求,修改Startup.cs的部分內容如下:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger,
    IWelcomeService welcomeService)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStaticFiles();

    //var welcome = config["Welcome"];

    app.Run(async (context) =>
    {
        var welcome = welcomeService.getMessage();
        logger.LogInformation(welcome);
        await context.Response.WriteAsync(welcome);
    });
}

重新啟動後可正常訪問到index.html

前面講到請求進入管道之後是安裝中介軟體新增順序處理的請求,如果當前中介軟體不能處理,才會交給下一個中介軟體,所以可以嘗試一下將上面的程式碼調整一下順序:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger,
    IWelcomeService welcomeService)
{
    if (env.IsDevelopment())
    {å
        app.UseDeveloperExceptionPage();
    }

    //var welcome = config["Welcome"];

    app.Run(async (context) =>
    {
        var welcome = welcomeService.getMessage();
        logger.LogInformation(welcome);
        await context.Response.WriteAsync(welcome);
    });

    app.UseStaticFiles();
}

可以看到StaticFiles放到了最後,這樣的話因為index.html請求會先到Run的地方,直接返回了,所以不能進入到StaticFiles裡,訪問得到的內容就是:

通過StaticFiles可以成功訪問到index.html,但是如果想要index.html成為預設網站主頁,需要使用中介軟體DefaultFiles,修改上面程式碼為:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger,
    IWelcomeService welcomeService)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseDefaultFiles();
    app.UseStaticFiles();

    //var welcome = config["Welcome"];

    app.Run(async (context) =>
    {
        var welcome = welcomeService.getMessage();
        logger.LogInformation(welcome);
        await context.Response.WriteAsync(welcome);
    });
}

DefaultFiles內部會自動將/修改為index.html然後交給其他中介軟體處理,所以需要放在StaticFiles的前面。

使用FileServer也可以實現同樣的效果:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    IConfiguration config, 
    ILogger<Startup> logger,
    IWelcomeService welcomeService)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseFileServer();

    //var welcome = config["Welcome"];

    app.Run(async (context) =>
    {
        var welcome = welcomeService.getMessage();
        logger.LogInformation(welcome);
        await context.Response.WriteAsync(welcome);
    });
}

中介軟體的一般註冊方式

除了使用內建的中介軟體之外,還可以用以下幾種方式註冊中介軟體:

  • Use
  • UseWhen
  • Map
  • MapWhen
  • Run

UseUseWhen註冊的中介軟體在執行完畢之後可以回到原來的管道上;
MapMapWhen可以在新的管道分支上註冊中介軟體,不能回到原來的管道上;
When的方法可以通過context做更多的中介軟體執行的條件;
Run用法和Use差不多,只不過不需要接收next引數,放在管道尾部;

例如實現返回對應路徑內容:

/// <summary>
/// 註冊應用程式所需的服務
/// </summary>
public void ConfigureServices(IServiceCollection service)
{
    
}

/// <summary>
/// 註冊管道中介軟體
/// </summary>
public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
    // 開發環境,新增開發者異常頁面
    if(env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Use 方式
    app.Use(async (context, next) =>
    {
        if(context.Request.Path == new PathString("/use"))
        {
            await context.Response.WriteAsync($"Path: {context.Request.Path}");
        }
        await next();
    });

    // UseWhen 方式
    app.UseWhen(context => context.Request.Path == new PathString("/usewhen"),
    a => a.Use(async (context, next) =>
    {
        await context.Response.WriteAsync($"Path: {context.Request.Path}");
        await next();
    }));

    // Map 方式
    app.Map(new PathString("/map"),
    a => a.Use(async (context, next) =>
    {
        // context.request.path 獲取不到正確的路徑
        //await context.Response.WriteAsync($"Path: {context.Request.Path}");
        await context.Response.WriteAsync($"PathBase: {context.Request.PathBase}");
        foreach(var item in context.Request.Headers)
        {
            await context.Response.WriteAsync($"\n{item.Key}: {item.Value}");
        }
    }));

    // MapWhen 方式
    app.MapWhen(context => context.Request.Path == new PathString("/mapwhen"),
    a => a.Use(async (context, next) =>
    {
        await context.Response.WriteAsync($"Path: {context.Request.Path}");
        await next();
    }));

    // Run 放在最後,可有可無,主要為了驗證是否可以回到原來的管道上繼續執行
    app.Run(async (context)=>
    {
        await context.Response.WriteAsync("\nCongratulation, return to the original pipe.");
    });
}

可以看到只有/use/usewhen可以執行到Run

注:這裡碰到一個問題,就是訪問/map路徑的時候獲取到的context.Request.Path為空,其他欄位獲取都挺正常,神奇。不過,可以使用context.Request.PathBase獲取到。

自己封裝中介軟體

對於上面註冊中介軟體的幾種方式,比如Use內部如果寫太多的程式碼也不合適,所以可以自己封裝中介軟體,封裝完成之後可以像內建中介軟體一樣使用UseXxxx的方式註冊。

本例目標要完成一箇中介軟體可以檢測HTTP請求方法,僅接受GETHEAD方法,步驟如下:
新建一個資料夾mymiddleware,新建檔案HttpMethodCheckMiddleware.cs,中介軟體封裝需要實現兩個方法:

  • HttpMethodCheckMiddleware: 建構函式,引數型別為RequestDelegate
  • Invoke: 中介軟體排程函式,引數型別為HttpContext,返回型別為Task

檔案內容如下:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace middleware.mymiddleware
{
    /// <summary>
    /// 請求方法檢查中介軟體,僅處理HEAD和GET方法
    /// </summary>
    public class HttpMethodCheckMiddleware
    {
        private readonly RequestDelegate _next;

        /// <summary>
        /// 構造方法,必須有的
        /// </summary>
        /// <param name="requestDelegate">下一個中介軟體</param>
        public HttpMethodCheckMiddleware(RequestDelegate requestDelegate)
        {
            this._next = requestDelegate;
        }

        /// <summary>
        /// 中介軟體排程方法
        /// </summary>
        /// <param name="context">HTTP上下文</param>
        /// <returns>TASK任務狀態</returns>
        public Task Invoke(HttpContext context)
        {
            // 如果符合條件,則將httpcontext傳給下一個中介軟體處理
            if(context.Request.Method.ToUpper().Equals(HttpMethods.Head)
                || context.Request.Method.ToUpper().Equals(HttpMethods.Get))
            {
                return _next(context);
            }

            // 否則直接返回處理完成
            context.Response.StatusCode = 400;
            context.Response.Headers.Add("X-AllowedHTTPVerb", new[] {"GET,HEAD"});
            context.Response.ContentType = "text/plain;charset=utf-8";  // 防止中文亂碼
            context.Response.WriteAsync("只支援GET、HEAD方法");
            return Task.CompletedTask;
        }
    }
}

這樣就可以直接在Startup中使用了:

app.UseMiddleware<HttpMethodCheckMiddleware>();

還可以編寫一個擴充套件類,封裝成類似內建中介軟體的方式UseXxx。新建CustomMiddlewareExtension.cs檔案,內容如下:

using Microsoft.AspNetCore.Builder;

namespace middleware.mymiddleware
{
    /// <summary>
    /// 封裝中介軟體的擴充套件類
    /// </summary>
    public static class CustomMiddlewareExtension
    {
        /// <summary>
        /// 新增HttpMethodCheckMiddleware中介軟體的擴充套件方法
        /// </summary>
        public static IApplicationBuilder UseHttpMethodCheckMiddleware(this IApplicationBuilder app)
        {
            return app.UseMiddleware<HttpMethodCheckMiddleware>();
        }
    }
}

現在就可以直接呼叫UseHttpMethodCheckMiddleware註冊中介軟體了.

執行結果截圖省略。

疑問:那個CustomMiddlewareExtension也沒見引用,怎麼就可以直接使用app.UseHttpMethodCheckMiddleware方法了?
有的可能和我一樣,c#都沒有學明白就直接開始擼dotnet了,看到這一臉懵逼,不過經過一番搜尋,原來這是c#中對已有類或介面進行方法擴充套件的一種方式,參考C#程式設計指南

內建路由

這一節先當了解,暫時用處不大,學完也會忘掉

先簡單看一下ASP.NET core內建的路由方式(直接上startup.cs程式碼內容):

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

namespace routing
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection servcies)
        {

        }

        public void Configure(IApplicationBuilder app)
        {
            // 新建一個路由處理器
            var trackPackageRouteHandler = new RouteHandler(context =>
            {
                var routeValues = context.GetRouteData().Values;
                return context.Response.WriteAsync($"Hello! Route values: {string.Join(", ", routeValues)}");
            });
            var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);
            // 通過MapRoute新增路由模板
            routeBuilder.MapRoute("Track Package Route", "package/{opration}/{id:int}");
            routeBuilder.MapGet("hello/{name}", context =>
            {
                var name = context.GetRouteValue("name");
                return context.Response.WriteAsync($"Hi, {name}!");
            });
            var routes = routeBuilder.Build();
            app.UseRouter(routes);
        }
    }
}

從程式碼中可知,需要先建立一個路由處理器trackPackageRouteHandler,然後通過RouteBuilderapptrackPackageRouteHandler繫結,而且需要新增一個匹配模板,最後將生成的路由器新增到app中。
其中新增路由匹配模板是使用了不同的方法:

  • MapRoute: 這個方法設定一個路由模板,匹配成功的請求會路由到trackPackageRouteHandler;
  • MapGet: 這個方法新增的模板,只適用於GET請求方式,並且第二個引數可以指定處理請求的邏輯;

上面設定路由的方式過於複雜,所以一般情況下通常使用MVC將對應的URL請求路由到Controller中處理,簡化路由規則。

Controller和Action

在開始MVC路由之前,先來學習一下ControllerAction他們的關係以及如何建立。

Controller一般是一些public類,Action對應Controller中的public函式,所以他們的關係也很明瞭:一個Controller可以有多個Action

Controller如何建立,預設情況下滿足下面的條件就可以作為一個Controller

  • 在專案根目錄的Controllers
  • 類名稱以Controller結尾並繼承自Controller,或被[Controller]標記的類
  • 共有類
  • 沒有被[NotController]被標記

例如一個Contoller的常用模式如下:

using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
    //...
}

Action就不需要許多條條框框了,只要寫在Controller中的方法函式都會被當成Action對待,如果不想一個函式被當做Action則需要新增[NotAction]標記。

留待測試:

  1. 如果同時新增[Controller][NotController]會發生什麼狀況?是誰在最後誰生效嗎還是報錯?
  2. 是不是隻需要滿足Controller字尾就可以了,不一定非得繼承Controller,繼承他只是為了使用一些已經打包好的父類函式。

MVC路由

首先建立一個HomeController測試路由用,需要建立到Controllers目錄下:

using Microsoft.AspNetCore.Mvc;

namespace routing.Controllers
{
    public class HomeController: Controller
    {
        public string Index()
        {
            return "Hello from HomeController.Index";
        }
    }
}

.net core 2.0.net core 3.0建立路由的方式有所不同,現在分開說一下,先說一下舊的方式。

先在ConfigureServices中註冊MVC服務,然後Configure中配置路由模板:

public void ConfigureServices(IServiceCollection service)
{
    // 註冊服務
    service.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
    if(env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    // 路由模板
    app.UseMvc(routes =>
    {
        routes.MapRoute(template: "{controller}/{action}/{id?}", 
                        defaults: new {controller = "Home", action = "Index"});
    });

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

但是放到dotnet3裡面是會報錯的:

MVCRouteStartup.cs(23,13): warning MVC1005: Using 'UseMvc' to configure MVC is not supported while using Endpoint Routing. To continue using 'UseMvc', please set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices'. 

提示UseMvc不支援Endpoint Routing,通過查資料(stackoverflow)找到原因,說的很清楚:2的時候MVC路由基於IRoute,3改成Endpoint了,官方推薦將UseMVC使用UseEndpoiont替換:

app.UseRouting(); // 必須寫,如果使用了UseStaticFiles要放在他之前
app.UseEndpoints(endpoionts =>
{
    endpoionts.MapControllerRoute(name: "MVC TEST ROUTE", 
                                pattern: "{controller}/{action}/{id?}",
                                defaults: new {controller = "Home", action = "Index"});
});

ConfigureServices中註冊MVC也有兩種方式:

services.AddMVC();

service.AddControllersWithViews();
service.AddRazorPages();

當然,如果不想把UseMap去掉,那麼可以按照報錯的提示在AddMVC的時候配置一下引數禁用EndpointRoute

services.AddMvc(options => options.EnableEndpointRouting = false);

然後就可以跑起來了:

好,扯了半天報錯,還是回到mvc路由上,上面是簡單演示了一下在Startup中如何建立路由,其實mvc路由有兩種定義方式:

  • 約定路由:上面使用的方式就是約定路由,需要在Startup中配置;
  • 特性路由:使用[Route]直接對controlleraction進行標記;

修改HomeController加上路由標記:

using Microsoft.AspNetCore.Mvc;

namespace routing.Controllers
{
    [Route("h")]
    [Route("[controller]")]
    public class HomeController: Controller
    {
        [Route("")]
        [Route("[action]")]
        public string Index()
        {
            return "Hello from HomeController.Index";
        }
    }
}

通過[controller][action]就可以動態的指代homeindex(路徑不區分大小寫),這樣如果路由會隨著類名或方法名稱的改變自動調整。

並且可以看出,可以多個[Route]標記重疊使用,例如訪問/h/home/index效果一樣:

通過實驗可以看出,特性路由會覆蓋掉約定路由

先總結這些吧,突然發現asp.net core這個東西還是挺先進的,比如依賴注入,Startup中的函式多數都是interface,為什麼直接對介面操作就可以改變一些東西或者讓我們可以自己註冊一箇中介軟體到app上,然後為什麼都不需要引用或者例項化就可以直接用app呼叫了,這都和依賴注入有關係吧,還有介面的設計理念也好像和其他語言的不太一樣,神奇了。

實驗程式碼

放到了github上,部分程式碼好像丟失了,不過應該不要緊。

相關文章