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

基於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.0html

準備

先到上面提供的下載地址,下載對應平臺的dotnet裝上,而後在命令行窗口輸入dotnet --version查看輸出是否安裝成功。c++

而後,安裝visual studio code,安裝以後還須要安裝C#拓展,要否則每次打開cs文件都會報錯。git

建立項目

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

使用命令dotnet new查看能夠新建的項目類型:web

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

項目結構與默認配置

目錄主要結構和文件功能以下:json

Program.cs是程序的主類,Main函數在這裏定義,內容大體能夠這麼理解:c#

CreateDefaultBuilder函數會使用默認的方法載入配置,例如經過讀取launchSettings.json肯定當前的發佈環境:瀏覽器

webhost經過ASPNETCORE_ENVIRONMENT讀取發佈環境,而後會讀取對應的配置文件,Development對應appsettings.Development.jsonProduction對應appsettings.jsonmvc

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上,部分代碼好像丟失了,不過應該沒關係。

相關文章
相關標籤/搜索