[鐵人賽 Day09] ASP.NET Core 2 系列 – Model Binding

ASP.NET Core MVC 的 Model Binding 會將 HTTP Request 資料,以映射的方式對應到相對到參數中。基本上跟 ASP.NET MVC 差不多,但能 Binding 的來源更多了一些。
本篇將介紹 ASP.NET Core 的 Model Binding。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day09] ASP.NET Core 2 系列 – Model Binding

Model Binding

要接收 Client 傳送來的資料,可以透過 Action 的參數接收,如下:

1
2
3
4
5
6
7
8
9
10
11
12
using Microsoft.AspNetCore.Mvc;

namespace MyWebsite.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index(int id)
        {
            return Content($"id: {id}");
        }
    }
}

id 就是從 HTTP Requset 的內容被 Binding 的 Model 參數。
預設的 Model Binding 會從 HTTP Requset 的三個地方取值 (優先順序由上到下) :

  • Form
    透過 HTTP POST 的 form 取值。如下圖:
    [鐵人賽 Day09] ASP.NET Core 2 系列 - Model Binding - Form
  • Route
    是透過 MVC Route URL 取值。
    如:http://localhost:5000/Home/Index/2id 取出的值就會是 2。
  • Query
    是透過 URL Query 參數取值。
    如:http://localhost:5000/Home/Index?id=1id 取出的值就會是 1。

如果三者都傳入的話,會依照優先順序取值 Form > Route > Query。

Binding Attributes

除了預設的三種 Binding 來源外,還可以透過 Model Binding Attributes 從 HTTP Requset 的其他資訊中 Binding。有以下 6 種類別:

  • [FromHeader]
    從 HTTP Header 取值。
  • [FromForm]
    透過 HTTP POST 的 form 取值。
  • [FromRoute]
    是透過 MVC Route URL 取值。
  • [FromQuery]
    是透過 URL Query 參數取值。
  • [FromBody]
    從 HTTP Body 取值,通常用於取 JSON, XML。
    ASP.NET Core MVC 預設的序列化是使用 JSON,如果要傳 XML 格式做 Model Binding 的話,要在 MVC 服務加入 XmlSerializerFormatters,如下:

    Startup.cs

    1
    2
    3
    4
    5
    6
    // ...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
                .AddXmlSerializerFormatters();
    }
  • [FromServices]
    這個比較特別,不是從 HTTP Requset 取值,而是從 DI 容器取值。
    DI 預設是使用 Constructor Injection,但 Controller 可能會因為每個 Action 用到不一樣的 Service 導致很多參數,所以也可以在 Action 注入 Service。

範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ...
public class HomeController : Controller
{
    public IActionResult FirstSample(
        [FromHeader]string header,
        [FromForm]string form,
        [FromRoute]string id,
        [FromQuery]string query)
    {
        return Content($"header: {header}, form: {form}, id: {id}, query: {query}");
    }
    
    public IActionResult DISample([FromServices] ILogger<HomeController> logger)
    {
        return Content($"logger is null: {logger == null}.");
    }

    public IActionResult BodySample([FromBody]UserModel model)
    {
        return Ok(model);
    }
}

// ...
public class UserModel
{
    public int Id { get; set; }        
    public string Name { get; set; }        
    public string Email { get; set; }        
    public string PhoneNumber { get; set; }        
    public string Address { get; set; }
}

輸出結果

FirstSample 輸出結果:
[鐵人賽 Day09] ASP.NET Core 2 系列 - Model Binding - Binding Attributes

DISample 輸出結果:
http://localhost:5000/Home/DISample

1
logger is null: False.

BodySample 輸出結果:

  • JSON
    [鐵人賽 Day09] ASP.NET Core 2 系列 - Model Binding - Binding Attributes
  • XML
    [鐵人賽 Day09] ASP.NET Core 2 系列 - Model Binding - Binding Attributes

Model 驗證

Model Binding 也可以順便幫忙驗證欄位資料,只要在資料模型的屬性上面帶上 Validation Attributes,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.ComponentModel.DataAnnotations;
// ...
public class UserModel
{
    [Required]
    public int Id { get; set; }

    [RegularExpression(@"\w+")]
    [StringLength(20, MinimumLength = 4)]
    public string Name { get; set; }

    [EmailAddress]
    public string Email { get; set; }

    [Phone]
    public string PhoneNumber { get; set; }

    [StringLength(200)]
    public string Address { get; set; }
}

然後在 Action 加上判斷:

Controllers\HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Microsoft.AspNetCore.Mvc;

namespace MyWebsite.Controllers
{
    public class HomeController : Controller
    {
        // ...
        public IActionResult BodySample([FromBody]UserModel model)
        {
            // 由於 Id 是 int 型別,int 預設為 0
            // 雖然有帶上 [Required],但不是 null 所以算是有值。
            if (model.Id < 1)
            {
                ModelState.AddModelError("Id", "Id not exist");
            }
            if (ModelState.IsValid)
            {
                return Ok(model);
            }
            return BadRequest(ModelState);
        }
    }
}

輸入錯誤資料的輸出結果:

[鐵人賽 Day09] ASP.NET Core 2 系列 - Model Binding - Model 驗證

.NET Core 提供了很多的 Validation Attributes,可以參考官網:System.ComponentModel.DataAnnotations

自製 Validation Attributes

如果 .NET Core 提供的 Validation Attributes 不夠用還可以自己做。
例如上述範例的資料模型多了生日欄位,需要驗證年齡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.ComponentModel.DataAnnotations;
// ...
public class UserModel
{
    [Required]
    public int Id { get; set; }

    [RegularExpression(@"\w+")]
    [StringLength(20, MinimumLength = 4)]
    public string Name { get; set; }

    [EmailAddress]
    public string Email { get; set; }

    [Phone]
    public string PhoneNumber { get; set; }

    [StringLength(200)]
    public string Address { get; set; }

    [DataType(DataType.Date)]
    [AgeCheck(18, 120)]
    public DateTime BirthDate { get; set; }
}

透過繼承 ValidationAttribute 就可以客製化自訂的 Model 驗證 Attributes,如下:

Attributes\AgeCheckAttribute.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.ComponentModel.DataAnnotations;

namespace MyWebsite.Attributes
{
    public class AgeCheckAttribute : ValidationAttribute
    {
        public int MinimumAge { get; private set; }
        public int MaximumAge { get; private set; }

        public AgeCheckAttribute(int minimumAge, int maximumAge)
        {
            MinimumAge = minimumAge;
            MaximumAge = maximumAge;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var date = Convert.ToDateTime(value);

            if (date.AddYears(MinimumAge) > DateTime.Today
                || date.AddYears(MaximumAge) < DateTime.Today)
            {
                return new ValidationResult(GetErrorMessage(validationContext));
            }

            return ValidationResult.Success;
        }

        private string GetErrorMessage(ValidationContext validationContext)
        {
            // 有帶 ErrorMessage 的話優先使用
            // [AgeCheck(18, 120, ErrorMessage="xxx")] 
            if (!string.IsNullOrEmpty(this.ErrorMessage))
            {
                return this.ErrorMessage;
            }

            // 自訂錯誤訊息
            return $"{validationContext.DisplayName} can't be in future";
        }
    }
}

參考

Overview of ASP.NET Core MVC
Introduction to model validation in ASP.NET Core MVC
ASP.NET CORE 2.0 MVC MODEL BINDING
ASP.NET CORE 2.0 MVC MODEL VALIDATION

[鐵人賽 Day08] ASP.NET Core 2 系列 – URL 重寫 (URL Rewrite)

路由跟 URL 重寫的功能性略有不同。路由是將 Request 找到對應的服務,而 URL 重寫是為了推卸責任 XD轉送 Request。
本篇將介紹 ASP.NET Core 的 URL 重寫 (URL Rewrite)。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day08] ASP.NET Core 2 系列 – URL 重寫 (URL Rewrite)

URL Rewrite 註冊

URL Rewriting Middleware 需要 Microsoft.AspNetCore.Rewrite 套件。
ASP.NET Core 2.0 以上版本,預設是參考 Microsoft.AspNetCore.All,已經包含 Microsoft.AspNetCore.Rewrite,所以不用再安裝。
URL 重寫功能是在 ASP.NET Core 1.1 之後的版本才有,如果是 ASP.NET Core 2.0 以前版本,可以透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.AspNetCore.Rewrite

要使用 URL 重寫,在 Startup.cs 的 Configure 對 IApplicationBuilder 使用 UseRewriter 方法註冊 URL Rewriting Middleware:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        var rewrite = new RewriteOptions()
            .AddRewrite("about.aspx", "home/about", skipRemainingRules: true)
            .AddRedirect("first", "home/index");
        app.UseRewriter(rewrite);
        // ...
    }
}

透過 RewriteOptions 建立 URL 重寫規則後,傳入給 URL Rewriting Middleware。
URL 重寫規則,主要有分兩種方式:

  • URL 重寫 (URL Rewrite)
    上例的 AddRewrite 就是 URL 重寫。
  • URL 轉址 (URL Redirect)
    上例的 AddRedirect 就是 URL 轉址。

URL 重寫

URL 重寫是屬於 Server 端的轉換事件,當 Client 端 Request 來的時候,發現原網址已經被換掉了,就會自動回傳新網址的內容。情境如下:

[鐵人賽 Day08] ASP.NET Core 2 系列 - URL 重寫 (URL Rewrite) - URL 重寫情境

上例 AddRewrite 有用到三個參數,當 URL 符合 參數 1 時,就將 參數 2 路由的內容回傳給 Client。
而 參數 3 是用來加速 URL 匹配的參數,類似 switch 的 break。若將 skipRemainingRules 設為 true,當找到匹配條件,就不再繼續往下找符合其他 參數 1 的規則。

  • 參數 1 支援正規表示式(Regular Expressions)。

範例結果:

[鐵人賽 Day08] ASP.NET Core 2 系列 - URL 重寫 (URL Rewrite) - URL 重寫 - 範例結果

URL 轉址

URL 重寫是屬於 Client 端的轉換事件,當 Client 端 Request 來的時候,發現原網址已經被換掉了,Server 會先回傳給 Client 告知新網址,再由 Client 重新 Request 新網址。情境如下:

[鐵人賽 Day08] ASP.NET Core 2 系列 - URL 重寫 (URL Rewrite) - URL 轉址情境

AddRedirect 的使用方式類似 AddRewrite,當 URL 符合 參數 1 時,就會回傳 參數 2 的 URL 給 Client。

  • 參數 1 同樣支援正規表示式(Regular Expressions)。

URL 轉址預設都是回傳 HTTP Status Code 302,也可以在 參數 3 指定回傳的 HTTP Status Code。
通常轉址的 HTTP Status Code 都是用 301 或 302 ,URL 轉址對 “人” 的行為來說沒有什麼意義,反正就是幫忙從 A 轉到 B;主要差異是給 “搜尋引擎” 理解的。

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        var rewrite = new RewriteOptions()
            .AddRedirect("first", "home/index", 301);
        app.UseRewriter(rewrite);
        // ...
    }
}
  • HTTP Status Code 301
    301 是要讓搜尋引擎知道,該網址已經永久轉移到另一個地方。
    通常用於網站搬家或網站改版,新舊版本路徑不相同,要重新對應的情況。
    範例結果:
    [鐵人賽 Day08] ASP.NET Core 2 系列 - URL 重寫 (URL Rewrite) - URL 重寫 - HTTP Status Code 301 範例結果
  • HTTP Status Code 302
    302 是告知搜尋引擎,雖然這次被轉址,但只是暫時性的。
    通常用於網站維護時,暫時原網址轉移到別的地方,如維修公告頁面。
    範例結果:
    [鐵人賽 Day08] ASP.NET Core 2 系列 - URL 重寫 (URL Rewrite) - URL 重寫 - HTTP Status Code 302 範例結果

正規表示式

AddRewrite 及 AddRedirect 都支援正規表示式的使用,且能把來源的 URL 透過正規表示式變成參數,帶入新 URL。

範例程式:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        var rewrite = new RewriteOptions()
            .AddRedirect("products.aspx?id=(\w+)", "prosucts/$1", 301);
            .AddRedirect("api/(.*)/(.*)/(.*)", "api?p1=$1&p2=$2&p3=$3", 301);
        app.UseRewriter(rewrite);
        // ...
    }
}
  • 當連到 http://localhost:5000/products.aspx?id=p123
    轉址到 http://localhost:5000/products/p123
  • 當連到 http://localhost:5000/api/first/second/third
    轉址到 http://localhost:5000/api?p1=first&p2=second&p3=third

透過正規表示式做 URL 轉址,對於網站新舊改版來說,非常好用。

參考

URL Rewriting Middleware in ASP.NET Core

[鐵人賽 Day07] ASP.NET Core 2 系列 – 路由 (Routing)

ASP.NET Core 透過路由(Routing)設定,將定義的 URL 規則找到相對應行為;當使用者 Request 的 URL 滿足特定規則條件時,則自動對應到相符的行為處理。
從 ASP.NET 就已經存在的架構,而且用法也很相似,只有些許的不同。
本篇將介紹 ASP.NET Core 的 Router Middleware 用法。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day07] ASP.NET Core 2 系列 – 路由 (Routing)

簡單路由

之前 [鐵人賽 Day03] ASP.NET Core 2 系列 – Middleware 有介紹到,可以透過 Map 處理一些簡單路由,例如:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        // ...
        app.Map("/first", mapApp =>
        {
            mapApp.Run(async context =>
            {
                await context.Response.WriteAsync("First. \r\n");
            });
        });
        app.Map("/second", mapApp =>
        {
            mapApp.Run(async context =>
            {
                await context.Response.WriteAsync("Second. \r\n");
            });
        });
    }
}

但要搭配 ASP.NET Core MVC 的話,簡單路由就沒這麼好用了。
RouterMiddleware 除了方便搭配 ASP.NET Core MVC 外,也可以比較彈性的使用路由定義。

路由註冊

RouterMiddleware 的路由註冊方式大致分為兩種:

  • 廣域註冊。如:MapRoute
  • 區域註冊。如:RouteAttribute

預設路由的順序如下:

[鐵人賽 Day07] ASP.NET Core 2 系列 - 路由(Routing) - 流程

路由的 Middleware 需要 Microsoft.AspNetCore.Routing 套件。
ASP.NET Core 2.0 以上版本,預設是參考 Microsoft.AspNetCore.All,已經包含 Microsoft.AspNetCore.Routing,所以不用再安裝。
如果是 ASP.NET Core 1.0 的版本,可以透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.AspNetCore.Routing

在 Startup.cs 的 ConfigureServices 加入 Routing 的服務,並在 Configure 定義路由規則:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRouting();
    }

    public void Configure(IApplicationBuilder app)
    {
        var defaultRouteHandler = new RouteHandler(context =>
        {
            var routeValues = context.GetRouteData().Values;
            return context.Response.WriteAsync($"Route values: {string.Join(", ", routeValues)}");
        });

        var routeBuilder = new RouteBuilder(app, defaultRouteHandler);
        routeBuilder.MapRoute("default", "{first:regex(^(default|home)$)}/{second?}");

        routeBuilder.MapGet("user/{name}", context => {
            var name = context.GetRouteValue("name");
            return context.Response.WriteAsync($"Get user. name: {name}");
        });

        routeBuilder.MapPost("user/{name}", context => {
            var name = context.GetRouteValue("name");
            return context.Response.WriteAsync($"Create user. name: {name}");
        });

        var routes = routeBuilder.Build();
        app.UseRouter(routes);
    }
}

可以看到上面程式碼,建立了兩個物件:

  • RouteHandler:這個物件如同簡單路由的 Run 事件,當路由成立的時候,就會執行這個事件。
  • RouteBuilder:在這個物件定義路由規則,當 Requset URL 符合規則就會執行該事件。
    • MapRoute:預設的路由規則,可以支援正規表示式(Regular Expressions)。
    • MapGet & MapPost
      HTTP Method 路由:同樣的 URL 可以透過不同的 HTTP Method,對應不同的事件。

第一個路由 MapRoute 定義:

  • URL 第一層透過正規表示式必需是 default 或 home,並放到路由變數 first 中。
  • URL 第二層可有可無,如果有的話,放到路由變數 second 中。

第二個路由 MapGet 定義:

  • 指定要是 HTTP Get
  • URL 第一層必需是 user
  • URL 第二層必需要有值,放到路由變數 name 中。

第三個路由 MapPost 定義:

  • 指定要是 HTTP Post
  • URL 第一層必需是 user
  • URL 第二層必需要有值,放到路由變數 name 中。

以上設定的路由結果如下:

  • http://localhost:5000/default 會顯示:
    Route values: [first, default]
  • http://localhost:5000/home/about 會顯示:
    Route values: [first, home], [second, about]
  • http://localhost:5000/user/john 透過 HTTP Get 會顯示:
    Get user. name: john
  • http://localhost:5000/user/john 透過 HTTP Post 會顯示:
    Create user. name: john

MVC 路由

MVC 路由使用跟上面範例差不多,只是把事件指向 Controller 及 Action
ASP.NET Core MVC 註冊路由規則的方式跟 ASP.NET MVC 差不多。
可以註冊多個 MapRoute,每個 Request 會經過這些 RouterMiddleware 找到對應 Action。
先被執行到的路由,後面就會被跳過,所以越廣域的寫越下面,如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "about",
                template: "about",
                defaults: new { controller = "Home", action = "About" }
            );
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}"
            );
            // 跟上面設定的 default 效果一樣
            //routes.MapRoute(
            //    name: "default",
            //    template: "{controller}/{action}/{id?}",
            //    defaults: new { controller = "Home", action = "Index" }
            //);
        });
    }
}

以上設定的路由結果如下:

  • http://localhost:5000 會對應到 HomeController 的 Index()。
  • http://localhost:5000/about 會對應到 HomeController 的 About()。
  • http://localhost:5000/home/test 會對應到 HomeController 的 Test()。

RouteAttribute

預設 RouteAttribute 的優先順序高於 Startup 註冊的 MapRoute,所以當使用 [Route] 後,原本的 MapRoute 將不再對 Controller 或 Action 產生作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Route("[controller]")]
public class UserController : Controller
{
    [Route("")]
    public IActionResult Profile()
    {
        return View();
    }

    [Route("change-password")]
    public IActionResult ChangePassword()
    {
        return View();
    }

    [Route("[action]")]
    public IActionResult Other()
    {
        return View();
    }
}

以上設定的路由結果如下:

  • http://localhost:5000/user 會對應到 UserController 的 Profile()。
  • http://localhost:5000/user/change-password 會對應到 UserController 的 ChangePassword()。
  • http://localhost:5000/user/other 會對應到 UserController 的 Other()。

若 Controller 設定了 [Route],Action 就要跟著加 [Route],不然會發生錯誤。

如果只有特定的 Action 需要改路由,也可以只加 Action。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserController : Controller
{
    public IActionResult Profile()
    {
        return View();
    }

    [Route("change-password")]
    public IActionResult ChangePassword()
    {
        return View();
    }

    public IActionResult Other()
    {
        return View();
    }
}
  • http://localhost:5000/user/profile 會對應到 UserController 的 Profile()。
  • http://localhost:5000/change-password 會對應到 UserController 的 ChangePassword()。
  • http://localhost:5000/user/other 會對應到 UserController 的 Other()。

注意!如果 [Route] 是設定在 Action,路徑是由網站根路徑開始算。

參考

Routing in ASP.NET Core

[鐵人賽 Day06] ASP.NET Core 2 系列 – MVC

ASP.NET Core MVC 跟 ASP.NET MVC 觀念是一致的,使用上也沒有什麼太大的變化。
過往 ASP.NET MVC 把 MVC 及 Web API 的套件分開,但在 ASP.NET Core 中 MVC 及 Web API 用的套件是相同的。
本篇將介紹 ASP.NET Core MVC 設定方式。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day06] ASP.NET Core 2 系列 – MVC

MVC 簡介

ASP.NET Core 的 MVC(Model-View-Controller) 架構模式延續 ASP.NET MVC,把網站分成三大元件 ModelViewController,相依關係如下圖:

[鐵人賽 Day06] ASP.NET Core 2 系列 - MVC - 相依關係

  • Model
    負責資料處理,包含資料存取、商業邏輯、定義資料物件及驗證資料。
  • View
    負責 UI 顯示,如 HTML、CSS 等介面設計配置。
  • Controller
    負責將使用者 Requset 找到相對應的 Model 及 View,做為控制流程的角色。

在 ASP.NET Core 中使用 MVC 或 Web API,需要 Microsoft.AspNetCore.Mvc 套件。
ASP.NET Core 2.0 以上版本,預設是參考 Microsoft.AspNetCore.All,已經包含 Microsoft.AspNetCore.Mvc,所以不用再安裝。
如果是 ASP.NET Core 1.0 的版本,可以透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.AspNetCore.Mvc

註冊 MVC 服務

安裝完成後,在 Startup.cs 的 ConfigureServices 加入 MVC 的服務,並在 Configure 對 IApplicationBuilder 使用 UseMvcWithDefaultRoute 方法註冊 MVC 預設路由的 Middleware。如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvcWithDefaultRoute();
    }
}

MVC 範例

Model

建立一個簡單的 Model 用於 Controller 跟 View 互動。

Models\UserModel.cs

1
2
3
4
public class UserModel
{
    public string Name { get; set; } = "John Wu";
}

Controller

在專案目錄下建立一個 Controllers 資料夾,把 Controller 都放這個目錄。
過去 ASP.NET 把 MVC 及 Web API 用的 Controller 分為 Controller 及 ApiController,現在 ASP.NET Core 把兩者合一,不再區分 ApiController
所以要建立一個類別,名稱後綴 Controller 即可,如下:

Controllers\HomeController.cs

1
2
3
4
5
6
7
public class HomeController
{
    public string Index()
    {
        return "This is HomeController.Index()";
    }
}

但要讓 Controller 跟 View 互動,還是需要繼承 Controller 比較方便,如下:

Controllers\HomeController.cs

1
2
3
4
5
6
7
8
public class HomeController : Controller
{
    public IActionResult Index()
    {
        var user = new UserModel();
        return View(model: user);
    }
}

IActionResult 回傳的方式可以有很多種,透過繼承 Controller 後,就可以使用 Controller 的方法:

  • View
    以上例來說,透過回傳 View 方法,可以找到該 Controller & Action 對應的 *.cshtml, 並且把 UserModel 傳給 View 使用。
  • HTTP Status Code
    回應包含 HTTP Status。常用的回應有 OkBadRequestNotFound 等。
    例如:return BadRequest("Internal Server Error"),會回應 HTTP Status 400 及 Internal Server Error 字串。
  • Redirect
    可以把 Request 轉給其他的 Action 或 URL。轉向的方法有 RedirectLocalRedirectRedirectToActionRedirectToRoute 等。
    例如:return RedirectToAction("Login", "Authentication"),就會把 Request 轉向到 AuthenticationController 的 Login()
  • Formatted Response
    回應時指定 Content-Type。Web API 的回傳通常都用這種方式,序列化物件順便標註 Content-Type
    例如:return Json(user),會將物件序列化成 JSON 字串,並在 HTTP Headers 帶上 Content-Type=application/json

View

View 跟 Controller 有相互的對應關係,預設在 Controller 使用 View 方法回傳結果,會從以下目錄尋找對應的 *.cshtml

  1. Views\{ControllerName}\{ActionName}.cshtml
    尋找與 Controller 同名的子目錄,再找到與 Action 同名的 *.cshtml
    如上例 HomeController.Index(),就會找專案目錄下的 Views\Home\Index.cshtml 檔案。
  2. Views\Shared\{ActionName}.cshtml
    如果 Controller 同名的子目錄,找不到 Action 同名的 *.cshtml。就會到 Shared 目錄找。 如上例 HomeController.Index(),就會找專案目錄下的 Views\Shared\Index.cshtml 檔案。

Views\Home\Index.cshtml

1
2
3
@model MyWebsite.Models.UserModel

Hello~ 我是 @Model.Name

在 *.cshtml 用 @model 綁定 Model 的型別,才可以使用 @Model 取得 Controller 傳入的物件。

範例結果

[鐵人賽 Day06] ASP.NET Core 2 系列 - MVC - 範例結果

資料流動畫如下:

[鐵人賽 Day06] ASP.NET Core 2 系列 - MVC - 資料流

參考

Overview of ASP.NET Core MVC
ASP.NET Core – Setup MVC

[鐵人賽 Day05] ASP.NET Core 2 系列 – 瀏覽靜態檔案 (Static Files)

過去 ASP.NET 網站,只要把 *.html*.css*.jpg*.png*.js 等靜態檔案放在專案根目錄,預設都可以直接被瀏覽;但 ASP.NET Core 小改了瀏覽靜態檔案的方式,預設根目錄不再能瀏覽靜態檔案,需要指定靜態檔案的目錄,才可以被瀏覽。
本篇將介紹 ASP.NET Core 瀏覽靜態檔案的方法。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day05] ASP.NET Core 2 系列 – 瀏覽靜態檔案 (Static Files)

試著在專案根目錄及 wwwroot 目錄中加入靜態檔案,例如:

專案目錄\index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>MyWebsite</title>
</head>
<body>
    專案 根目錄的 index.html
</body>
</html>

專案目錄\wwwroot\index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>MyWebsite</title>
</head>
<body>
    wwwroot 目錄的 index.html
</body>
</html>

然後在網址列輸入:

  • http://localhost:5000/index.html
  • http://localhost:5000/wwwroot/index.html
    會發現以上兩個連結都沒有辦法開啟 index.html。

瀏覽靜態檔案,需要 Microsoft.AspNetCore.StaticFiles 套件。
ASP.NET Core 2.0 以上版本,預設是參考 Microsoft.AspNetCore.All,已經包含 Microsoft.AspNetCore.StaticFiles,所以不用再安裝。
如果是 ASP.NET Core 1.0 的版本,可以透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.AspNetCore.StaticFiles

啟用靜態檔案

在 Startup.cs 的 Configure 對 IApplicationBuilder 使用 UseStaticFiles 方法註冊靜態檔案的 Middleware:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();

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

UseStaticFiles 預設啟用靜態檔案的目錄是 wwwroot,設定完成後再次嘗試開啟 URL:

  • http://localhost:5000/index.html
    開啟的內容會是:wwwroot 目錄的 index.html
  • http://localhost:5000/wwwroot/index.html
    依然無法顯示靜態檔案。

UseStaticFiles 註冊的順序可以在外層一點,比較不會經過太多不必要的 Middleware。如圖:

[鐵人賽 Day05] ASP.NET Core 2 系列 - 瀏覽靜態檔案

當 Requset 的 URL 檔案不存在,則會轉向到 Run 的事件(如灰色箭頭)。

變更網站目錄

預設網站目錄是 wwwroot,如果想要變更此目錄,可以在 Program.cs 的 WebHost Builder 用 UseWebRoot 設定網站預設目錄。
例如:把預設網站目錄 wwwroot 改為 public,如下:

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace MyWebsite
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseWebRoot("public")
                .UseStartup<Startup>()
                .Build();
    }
}

啟用指定目錄

由於 UseStaticFiles 只能拿到預設資料夾底下的檔案,某些情況會需要特定目錄也能使用靜態檔案。
例如:用 npm 安裝的套件都放在專案目錄底下的 node_modules

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseStaticFiles();
        app.UseStaticFiles(new StaticFileOptions()
        {
            FileProvider = new PhysicalFileProvider(
                Path.Combine(env.ContentRootPath, @"node_modules")),
            RequestPath = new PathString("/third-party")
        });
        // ...
    }
}

以上設定就會把 URL http://localhost:5000/third-party/example.js 指向到 專案目錄\node_modules\example.js

預設檔案

比較友善的使用者經驗會希望 http://localhost:5000/ 可以自動指向到 index.html。
能透過 UseDefaultFiles 設定靜態檔案目錄的預設檔案。

Startup.cs

1
2
3
4
5
6
7
8
9
10
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseDefaultFiles();
        app.UseStaticFiles();
        // ...
    }
}
  • UseDefaultFiles的職責是嘗試請求預設檔案。
  • UseStaticFiles 的職責是回傳請求的檔案。

UseDefaultFiles 必須註冊在 UseStaticFiles 之前。
如果先註冊 UseStaticFiles,當 URL 是 / 時,UseStaticFiles 找不到該檔案,就會直接回傳找不到;所以就沒有機會進到 UseDefaultFiles

自訂預設檔案

UseDefaultFiles的預設檔案如下:

  • default.htm
  • default.html
  • index.htm
  • index.html

如果預設檔案的檔名不在上列清單,也可以自訂要用什麼名稱當作預設檔案。
透過 DefaultFilesOptions 設定後,傳入 UseDefaultFiles

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        var defaultFilesOptions = new DefaultFilesOptions();
        defaultFilesOptions.DefaultFileNames.Add("custom.html");
        app.UseDefaultFiles(defaultFilesOptions);
        app.UseStaticFiles();
        // ...
    }
}

檔案清單

基本上為了網站安全性考量,不應該讓使用者瀏覽伺服器上面的檔案清單,但如果真有需求要讓使用者瀏覽檔案清單也不是不行。

在 Startup.cs 的 Configure 對 IApplicationBuilder 使用 UseFileServer 方法註冊檔案伺服器的功能:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseFileServer(new FileServerOptions()
        {
            FileProvider = new PhysicalFileProvider(
                Path.Combine(env.ContentRootPath, @"bin")
            ),
            RequestPath = new PathString("/StaticFiles"),
            EnableDirectoryBrowsing = true
        });
    }
}

當連入 http://localhost:5000/StaticFiles 時,就指向到 專案目錄\bin\ 目錄,並且可以直接瀏覽檔案目錄及檔案內,如下:

[鐵人賽 Day05] ASP.NET Core 2 系列 - 瀏覽檔案清單

參考

Working with static files in ASP.NET Core

軟體分層架構模式

軟體分層架構模式 - 基本分層

最近在重構六年前做的產品,雖然當時已經有做分層架構,但還是有很多該改進的地方。
有些命名越看越不順眼,重構期間順便整理一下分層架構;不管在何時回頭看自己做的東西,都覺得很多進步空間。
本篇介紹一下常見的軟體分層架構模式 (Software Layered Architecture Pattern),以及推薦的命名方式。

分層架構簡介

基本分層

基本分層架構模式主分為:

  • 展示層 (Presentation Layer)
    • UI 互動相關的部分
  • 業務層 (Business Layer)
    • 處理業務邏輯的部分
  • 資料層 (Data Layer)
    • 處理資料存取的部分

在 Software Architecture Patterns – O’Reilly 書中 資料層 (Data Layer) 被分為 Persistence Layer 及 Database Layer,我個人比較喜歡 Microsoft Application Architecture Guide 用 Data Layer 的命名方式。畢竟資料來源不一定是資料庫,也可能是外部的 Services。

分層架構有一個很重要的特性,就是要把每一層的職責分離,不應該跨層互動,每層之間的關係只能是上下互動。
如圖:

軟體分層架構模式 - 基本分層

服務型分層

上述的三層為了做到職責分離,只能層層互動,卻缺少了一些彈性。如果要提供 API 給外部使用,就處於比較尷尬的位置;不屬於展示層,比較偏向業務層,但業務層直接打破隔離方式供人使用也怪怪的。
所以如果是服務型 (Service-Based) 的系統,會建議多出一層:

  • 服務層 (Service Layer)
    • 負責把封閉的分層開放給外部使用。

如圖:

軟體分層架構模式 - 服務型分層

命名方式

從網路上可以找到很多不同風格的命名方式,此章節只是我整理出我喜歡的命名風格,如果還沒有命名頭緒的話可以參考看看。 我大部分時間都是在開發 ASP.NET MVC/WebAPI2 所以會以 .NET 專案 為例。

Domain Project

專案相依:不應該相依於其他專案。
專案名稱:CompanyName.ProjectName.Domain

這個專案主要是用來分離各層相依關係的,內容含如下:

建議的命名範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// DTO 範例
// Class 命名規則:不後綴 Entity。  
namespace CompanyName.ProjectName.Domain.Entities
{
  public class User {
    // ...
  }
}

// Enum 範例
// Class 命名規則:不後綴 Enum。 
namespace CompanyName.ProjectName.Domain.Enums
{
  public enum UserStatus {
    // ...
  }
}

Data Layer

專案相依:CompanyName.ProjectName.Domain
專案名稱:CompanyName.ProjectName.DataLayer

常見的命名有:UserDAL、UserEngine、UserManager、UserRepository 等。

DAL 全名 Data Access Layer,名稱應該是從 3-tier Architecture with ASP.NET 2.0 誕生出來的。

建議的命名Class 命名規則:名稱加上後綴 Manager
如果有用 Repository Pattern,就在 Class 名稱加上後綴 Repository
範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Class 命名規則:加上後綴 Manager。 
namespace CompanyName.ProjectName.DataLayer.Managers
{
  public class RedisManager : IRedisManager {
    // ...
  }
}

// Repository Pattern 範例。 
// Class 命名規則:加上後綴 Repository 
namespace CompanyName.ProjectName.DataLayer.Repositorys
{
  public class UserRepository : IUserRepository {
    // ...
  }
}

Business Layer

專案相依:CompanyName.ProjectName.Domain 及 CompanyName.ProjectName.DataLayer 專案名稱:CompanyName.ProjectName.BusinessLayer

常見的命名有:UserBLL、UserLogic 等。

BLL 全名是 Business Logic Layer,名稱出現同 DAL。

建議的命名:Class 名稱加上後綴 Logic

網路上非常多的範例適用 BLL,尤其是 ASP.NET 的範例。
但我不推用 BLL 的原因是,Class 名稱出現連續的全大寫,看久了有點不舒服,還是比較習慣 Pascal Case

範例:

1
2
3
4
5
6
namespace CompanyName.ProjectName.BusinessLayer.Logics
{
  public class UserLogic : IUserLogic {
    // ...
  }
}

Service Layer

專案相依:CompanyName.ProjectName.Domain 及 CompanyName.ProjectName.BusinessLayer

API Library

由於 Service Layer 是屬於對外開放的接口,所以我並沒有特別推薦命名方式,不要太突兀就好。
可以參考許多第三方套件的 API 命名方式,例如常見的 Newtonsoft.Json

1
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { });

JsonConvert 就沒有特別加什麼後綴,用比較直觀式的命名方式,讓使用方容易懂就好。

Web API

專案如果是 Web API,我會直接取名為 CompanyName.ProjectName.WebService
命名方式建議使用 RESTful 風格,用起來比較乾淨俐落,好處可以參考 Wiki。

前陣子有人問我:

如果把 Web API 符合 RESTful,是不就變成 Data Layer 了?

這兩個層級的職責是完全不一樣的:

  • Service Layer:提供資源給外部使用,負責轉手資料。
  • Data Layer:負責提供及存取 Business Layer 收送的資料。

但如果從另一個角度來看,對調用方來說,Web API 也是它的 Data Layer,把 Web API 符合 RESTful 只是為了讓調用方更容易使用。

Presentation Layer

專案相依:CompanyName.ProjectName.Domain 及 CompanyName.ProjectName.ServiceLayer (或 CompanyName.ProjectName.BusinessLayer ) 專案名稱:CompanyName.ProjectName.Website

以 Web 專案來說,這層是屬與 HTML、jQuery 或 Angular 這類的前端框架。
如果有用前端框架,命名方式就依照該框架建議的指南命名。

如果是純前端框架,其實根本不用相宜於任何專案,是以 Web API 作為相依關係。

範例架構

專案相依關係:

軟體分層架構模式 - 專案相依關係

檔案架構大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CompanyName.ProjectName.Domain/         # Domain 專案
  Entities/
    User.cs
  Enums/
    UserStatus.cs
  Interfaces/                           # 分層隔離用到的介面
    Logics/
      IUserLogic.cs
    Managers/
      IRedisManager.cs
    Repositorys/
      IUserRepository.cs

CompanyName.ProjectName.DataLayer/      # Data Layer 專案
  Managers/
    RedisManager.cs
  Repositorys/
    UserRepository.cs

CompanyName.ProjectName.BusinessLayer/  # Business Layer 專案
  Logics/
    UserLogic.cs

CompanyName.ProjectName.WebService/     # Service Layer 專案
  Controllers/
    UserController.cs

CompanyName.ProjectName.Website/        # Angular 為例
  index.html                            # 起始頁面
  app/                                  # Angular 的主要目錄
    main.ts                             # bootstrap 的程式進入點

參考

Software Architecture Patterns – O’Reilly Media(推薦閱讀)
Chapter 5: Layered Application Guidelines – Microsoft Application Architecture Guide
Naming conventions DAL, BAL, and UI Layer
應用程式的分層設計 (1) – 入門範例

[鐵人賽 Day03] ASP.NET Core 2 系列 – Middleware

過去 ASP.NET 中使用的 HTTP Modules 及 HTTP Handlers,在 ASP.NET Core 中已不復存在,取而代之的是 Middleware。
Middleware 除了簡化了 HTTP Modules/Handlers 的使用方式,還帶入了 Pipeline 的概念。
本篇將介紹 ASP.NET Core 的 Middleware 概念及用法。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day03] ASP.NET Core 2 系列 – Middleware

Middleware 概念

ASP.NET Core 在 Middleware 的官方說明中,使用了 Pipeline 這個名詞,意旨 Middleware 像水管一樣可以串聯在一起,所有的 Request 及 Response 都會層層經過這些水管。
用圖例可以很容易理解,如下圖:

[鐵人賽 Day03] ASP.NET Core 2 系列 - Middleware - 概念

App.Use

Middleware 的註冊方式是在 Startup.cs 的 Configure 對 IApplicationBuilder 使用 Use 方法註冊。
大部分擴充的 Middleware 也都是以 Use 開頭的方法註冊,例如:

  • UseMvc():MVC 的 Middleware
  • UseRewriter():URL rewriting 的 Middleware

一個簡單的 Middleware 範例。如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("First Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("First Middleware out. \r\n");
        });

        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("Second Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("Second Middleware out. \r\n");
        });

        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("Third Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("Third Middleware out. \r\n");
        });

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

用瀏覽器打開網站任意連結,輸出結果:

1
2
3
4
5
6
7
First Middleware in. 
Second Middleware in. 
Third Middleware in. 
Hello World! 
Third Middleware out. 
Second Middleware out. 
First Middleware out.

在 Pipeline 的概念中,註冊順序是很重要的事情。資料經過的順序一定是先進後出

Request 流程如下圖:

[鐵人賽 Day03] ASP.NET Core 2 系列 - Middleware

Middleware 也可以作為攔截使用,如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("First Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("First Middleware out. \r\n");
        });

        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("Second Middleware in. \r\n");
            
            // 水管阻塞,封包不往後送
            var condition = false;
            if(condition) {
                await next.Invoke();
            }

            await context.Response.WriteAsync("Second Middleware out. \r\n");
        });

        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("Third Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("Third Middleware out. \r\n");
        });

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

輸出結果:

1
2
3
4
First Middleware in. 
Second Middleware in. 
Second Middleware out. 
First Middleware out.

在 Second Middleware 中,因為沒有達成條件,所以封包也就不在往後面的水管傳送。流程如圖:

[鐵人賽 Day03] ASP.NET Core 2 系列 - Middleware - 概念

App.Run

Run 是 Middleware 的最後一個行為,以上面圖例來說,就是最末端的 Action。
它不像 Use 能串聯其他 Middleware,但 Run 還是能完整的使用 Request 及 Response。

App.Map

Map 是能用來處理一些簡單路由的 Middleware,可依照不同的 URL 指向不同的 Run 及註冊不同的 Use
新增一個路由如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) => 
        {
            await context.Response.WriteAsync("First Middleware in. \r\n");
            await next.Invoke();
            await context.Response.WriteAsync("First Middleware out. \r\n");
        });

        app.Map("/second", mapApp =>
        {
            mapApp.Use(async (context, next) => 
            {
                await context.Response.WriteAsync("Second Middleware in. \r\n");
                await next.Invoke();
                await context.Response.WriteAsync("Second Middleware out. \r\n");
            });
            mapApp.Run(async context =>
            {
                await context.Response.WriteAsync("Second. \r\n");
            });
        });

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

開啟網站任意連結,會顯示:

1
2
3
First Middleware in. 
Hello World! 
First Middleware out.

開啟網站 http://localhost:5000/second,則會顯示:

1
2
3
4
5
First Middleware in. 
Second Middleware in. 
Second. 
Second Middleware out. 
First Middleware out.

建立 Middleware 類別

如果 Middleware 全部都寫在 Startup.cs,程式碼應該很難維護,所以應該把自製的 Middleware 邏輯獨立出來。
建立 Middleware 類別不需要額外繼承其它類別或介面,一般的類別即可,範例如下:

FirstMiddleware.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FirstMiddleware
{
    private readonly RequestDelegate _next;

    public FirstMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await context.Response.WriteAsync($"{nameof(FirstMiddleware)} in. \r\n");

        await _next(context);

        await context.Response.WriteAsync($"{nameof(FirstMiddleware)} out. \r\n");
    }
}

全域註冊

在 Startup.Configure 註冊 Middleware 就可以套用到所有的 Request。如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<FirstMiddleware>();
        // ...
    }
}

區域註冊

Middleware 也可以只套用在特定的 Controller 或 Action。註冊方式如下:

Controllers\HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
// ..
[MiddlewareFilter(typeof(FirstMiddleware))]
public class HomeController : Controller
{
    // ...

    [MiddlewareFilter(typeof(SecondMiddleware))]
    public IActionResult Index()
    {
        // ...
    }
}

Extensions

大部分擴充的 Middleware 都會用一個靜態方法包裝,如:UseMvc()UseRewriter()等。
自製的 Middleware 當然也可以透過靜態方法包,範例如下:

Extensions\CustomMiddlewareExtensions.cs

1
2
3
4
5
6
7
public static class CustomMiddlewareExtensions
{
    public static IApplicationBuilder UseFirstMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FirstMiddleware>();
    }
}

註冊 Extension Middleware 的方式如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
// ...
public class Startup
{
    // ...
    public void Configure(IApplicationBuilder app)
    {
        app.UseFirstMiddleware();
        // ...
    }
}

參考

ASP.NET Core Middleware Fundamentals
Creating Custom Middleware In ASP.Net Core

[鐵人賽 Day02] ASP.NET Core 2 系列 – 程式生命週期 (Application Lifetime)

要了解程式的運作原理,要先知道程式的進入點及生命週期。
過往 ASP.NET MVC 啟動方式,是繼承 HttpApplication 做為網站開始的進入點。
ASP.NET Core 改變了網站啟動的方式,變的比較像是 Console Application。
本篇將介紹 ASP.NET Core 的程式生命週期 (Application Lifetime) 及補捉 Application 停啟事件。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day02] ASP.NET Core 2 系列 – 程式生命週期 (Application Lifetime)

程式進入點

.NET Core 把 Web 及 Console 專案都變成一樣的啟動方式,預設從 Program.cs 的 Program.Main 做為程式進入點,再從程式進入點把 ASP.NET Core 網站實例化。
我個人是覺得比 ASP.NET MVC 繼承 HttpApplication 的方式簡潔許多。

透過 .NET Core CLI 建置的 Program.cs 內容大致如下:
Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace MyWebsite
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}

Program.Main 透過 BuildWebHost 方法取得 WebHost 後,再啟動 WebHost;WebHost 就是 ASP.NET Core 的網站實體。

  • WebHost.CreateDefaultBuilder
    透過此方法建立 WebHost Builder。WebHost Builder 是用來產生 WebHost 的物件。
    可以在 WebHost 產生之前設定一些前置準備動作,當 WebHost 建立完成時,就可以使用已準備好的物件等。
  • UseStartup
    設定該 Builder 產生的 WebHost 啟動後,要執行的類別。
  • Build
    當前置準備都設定完成後,就可以跟 WebHost Builder 呼叫此方法實例化 WebHost,並得到該實例。
  • Run
    啟動 WebHost。

Startup.cs

當網站啟動後,WebHost 會實例化 UseStartup 設定的 Startup 類別,並且呼叫以下兩個方法:

  • ConfigureServices
  • Configure

透過 .NET Core CLI 建置的 Startup.cs 內容大致如下:
Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // ...
        }

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

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

對 WebHost 來說 Startup.cs 並不是必要存在的功能。
可以試著把 Startup.cs 中的兩個方法,都改成在 WebHost Builder 設定,變成啟動的前置準備。如下:

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

namespace MyWebsite
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureServices(services =>
                {
                    // ...
                })
                .Configure(app =>
                {
                    app.Run(async (context) =>
                    {
                        await context.Response.WriteAsync("Hello World!");
                    });
                })
                .Build();
    }
}

把 ConfigureServices 及 Configure 都改到 WebHost Builder 註冊,網站的執行結果會是一樣的。

兩者之間最大的差異就是呼叫時間點不同。

  • 在 WebHost Builder 註冊,是在 WebHost 實例化之前呼叫。
  • 在 Startup.cs 註冊,是在 WebHost 實例化之後呼叫。

但 Configure 無法使用除了 IApplicationBuilder 以外的參數。
因為在 WebHost 實例化前,自己都還沒被實例化,怎麼可能會有物件能注入給 Configure

Application Lifetime

除了程式進入點外,WebHost 的停起也是網站事件很重要一環,ASP.NET Core 不像 ASP.NET MVC 用繼承的方式補捉啟動及停止事件。 是透過 Startup.Configure 注入 IApplicationLifetime 來補捉 Application 停啟事件。

IApplicationLifetime 有三個註冊監聽事件及終止網站事件可以觸發。如下:

1
2
3
4
5
6
7
public interface IApplicationLifetime
{
  CancellationToken ApplicationStarted { get; }
  CancellationToken ApplicationStopping { get; }
  CancellationToken ApplicationStopped { get; }
  void StopApplication();
}
  • ApplicationStarted
    當 WebHost 啟動完成後,會執行的啟動完成事件
  • ApplicationStopping
    當 WebHost 觸發停止時,會執行的準備停止事件
  • ApplicationStopped
    當 WebHost 停止事件完成時,會執行的停止完成事件
  • StopApplication
    可以透過此方法主動觸發終止網站

IApplicationLifetime 需要 Microsoft.AspNetCore.Hosting 套件。
不過 ASP.NET Core 2.0 以上版本,預設是參考 Microsoft.AspNetCore.All,已經包含 Microsoft.AspNetCore.Hosting,所以不用再安裝。
如果是 ASP.NET Core 1.0 的版本,可以透過 .NET Core CLI 在專案資料夾執行安裝指令:

1
dotnet add package Microsoft.AspNetCore.Hosting

範例程式

透過 Console 輸出執行的過程,範例如下:
Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace MyWebsite
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Output("Application - Start");
            var webHost = BuildWebHost(args);
            Output("Run WebHost");
            webHost.Run();
            Output("Application - End");
        }

        public static IWebHost BuildWebHost(string[] args)
        {
            Output("Create WebHost Builder");
            var webHostBuilder = WebHost.CreateDefaultBuilder(args)
                .ConfigureServices(services =>
                {
                    Output("webHostBuilder.ConfigureServices - Called");
                })
                .Configure(app =>
                {
                    Output("webHostBuilder.Configure - Called");
                })
                .UseStartup<Startup>();

            Output("Build WebHost");
            var webHost = webHostBuilder.Build();

            return webHost;
        }

        public static void Output(string message)
        {
            Console.WriteLine($"[{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}] {message}");
        }
    }
}

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
    public class Startup
    {
        public Startup()
        {
            Program.Output("Startup Constructor - Called");
        }

        public void ConfigureServices(IServiceCollection services)
        {
            Program.Output("Startup.ConfigureServices - Called");
        }

        public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime)
        {
            appLifetime.ApplicationStarted.Register(() =>
            {
                Program.Output("ApplicationLifetime - Started");
            });

            appLifetime.ApplicationStopping.Register(() =>
            {
                Program.Output("ApplicationLifetime - Stopping");
            });

            appLifetime.ApplicationStopped.Register(() =>
            {
                Thread.Sleep(5 * 1000);
                Program.Output("ApplicationLifetime - Stopped");
            });

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

            // For trigger stop WebHost
            var thread = new Thread(new ThreadStart(() =>
            {
                Thread.Sleep(5 * 1000);
                Program.Output("Trigger stop WebHost");
                appLifetime.StopApplication();
            }));
            thread.Start();

            Program.Output("Startup.Configure - Called");
        }
    }
}

執行結果

[鐵人賽 Day02] ASP.NET Core 2 系列 - 程式生命週期 (Application Lifetime) - 執行結果

輸出內容少了 webHostBuilder.Configure – Called,因為 Configure 只能有一個,後註冊的 Configure 會把之前註冊的蓋掉。

物件執行流程如下:

[鐵人賽 Day02] ASP.NET Core 2 系列 - 程式生命週期 (Application Lifetime) - 物件執行流程

參考

Application startup in ASP.NET Core
Hosting in ASP.NET Core

[鐵人賽 Day01] ASP.NET Core 2 系列 – 從頭開始

來勢洶洶的 .NET Core 似乎要取代 .NET Framework,ASP.NET 也隨之發佈 .NET Core 版本。雖說名稱沿用 ASP.NET,但相較於 ASP.NET 確有許多架構上的差異,可說是除了名稱外,已是兩個不同的框架。
本系列文將介紹 ASP.NET Core 入門教學及一些實務運用的範例,本篇主要介紹基本的 ASP.NET Core 環境準備及如何用 Visual Studio Code (VS Code) 開發 ASP.NET Core。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day01] ASP.NET Core 2 系列 – 從頭開始

前言

要開發 .NET Core 必需要安裝 .NET Core SDK,所以先到官網下載 .NET Core SDK 的安裝檔,官網下載位置點我

.NET Core 是跨作業系統的框架,不再像 .NET Framework 要依附在 Windows 的作業系統才能執行,所以你可以依照你需要的版本進行下載及安裝。
雖然我的電腦是 Windows 作業系統,但接下來的系列教學都會是以指令為主。
(安裝軟體步驟太簡單,除了按下一步以外,幾乎沒什麼好解說的,所以不介紹怎麼安裝軟體。)

安裝完成後,可以透過 .NET Core CLI (Command-Line Interface)確認 .NET Core SDK 安裝的版本,指令如下:

1
dotnet --version

建立網站專案

先建立一個專案資料夾 MyWebsite,然後在該資料夾執行 .NET Core CLI 建置網站的指令:

1
dotnet new web

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - 建立專案

.NET Core CLI 會在該資料夾,建立一個空的 ASP.NET Core 專案,內容如下:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - 專案目錄

1
2
3
4
5
obj/                            # 專案暫存目錄
wwwroot/                        # 預設網站根目錄 (空的)
MyWebsite.csproj                # 專案檔
Program.cs                      # 程式進入檔
Startup.cs                      # 啟動網站設定

啟動網站

建立完成後,就可以用 .NET Core CLI 啟動網站了。啟動網站指令:

1
dotnet run

.NET Core CLI 預設會起一個http://localhost:5000/的站台,用瀏覽器打開此連結就可以看到 ASP.NET Core 網站了。如下:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - 啟動網站

Visual Studio Code

.NET Core 都已經跨作業系統了,開發工具當然也就不再限制於 Visual Studio IDE (Visual Studio 2017/2015 等)。基本上純文字編輯器搭配 .NET Core CLI 就可以開發 ASP.NET Core 了,但沒有中斷點除錯或 Autocomplete 開發有些辛苦。如果是 Windows 作業系統,最推薦的當然還是 Visual Studio IDE,再來就是 Visual Studio Code (簡稱 VS Code)。

VS Code 是一套可安裝擴充套件的文字編輯器,有支援 Windows、Mac 及 Linux 版本,極輕量又免費。
只要安裝擴充套件就變成了 IDE,並且支援多種不同的程式語言。下載位置點我

安裝擴充套件

打開 VS Code 可以在左邊看到五個 Icon,點選最下面的那個 Extensions 圖示,並在 Extensions 搜尋列輸入 C# ,便可以找到 C# 的擴充套件安裝。如下圖:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code C# 擴充套件

開啟專案

VS Code 跟一般文字編輯器有些不同,它是以資料夾為工作區域,開啟一個目錄,就等通於是開啟一個專案。從上方工具列 File -> Open Folder 選擇 ASP.NET Core 專案目錄,大概隔幾秒後,VS Code 會提示是否要幫此專案加入 Build/Debug 的設定。如下圖:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code 開啟專案

Build/Debug 設定

如果沒有自動提示加入 Build/Debug 設定,可以在左邊 Icon,點選倒數第二個 Debug 圖示,手動加入 Build/Debug 設定。如下步驟:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code Build/Debug 設定[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code Build/Debug 設定

設定完成後,VS Code 會自動建立 .vscode 目錄及設定檔 launch.jsontasks.json。目錄結構如下:

1
2
3
4
5
6
7
8
.vscode/                        # VS Code 設定檔目錄
  launch.json                   # 用 VS Code 啟動程式的設定檔
  tasks.json                    # 定義 launch.json 會用道的指令設定檔
obj/                            # 專案暫存目錄
wwwroot/                        # 預設網站根目錄 (空的)
MyWebsite.csproj                # 專案檔
Program.cs                      # 程式進入點
Startup.cs                      # 啟動網站設定

中斷點除錯

在程式碼行號左邊點擊滑鼠就可以下中斷點了,跟一般 IDE 差不多。然後在 Debug 側欄啟動偵錯:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code 中斷點除錯

當執行到該中斷點後,就會停下來,並在 Debug 側欄顯示當前變數狀態等,也可以用滑鼠移到變數上面檢視該變數的內容。如下:

[鐵人賽 Day01] ASP.NET Core 2 系列 - 從頭開始 - VS Code 中斷點除錯

偵錯方式跟大部分的 IDE 都差不多,可以 Step over、Step in/out 等。
如此一來就可以用 VS Code 輕鬆開發 ASP.NET Core。

ProGet – 架設內部 NuGet Server

系統規模較大或模組較多時,並不適合用專案相依,避免編譯太久及程式碼管理的問題等。
常見的方式是將 DLL 編譯出來,再給需要的專案參考,但同步 DLL 的過程需要控管,以免拿錯版本。
比較好的方式是透過 Dependency Service 解決專案相依的問題,而 .NET 的 Dependency Service 主要是 NuGet。
本篇介紹如何透過 ProGet 架設內部 NuGet Server。

前言

ProGet 是一套支援多種 Dependency Service 工具,它支援以下 Feed 服務:

  • Bower
  • Chocolatey
  • Docker
  • Maven
  • npm
  • NuGet (本篇重點)
  • PowerShell
  • Ruby Gems
  • Universal
  • VSIX

並且有 Windows 版本及 Linux(Docker) 版本可以架設,本篇重點將以 Windows 版本架設為主。
ProGet 有分付費版免費版,免費版支援的 Feed 跟付費版一樣,功能也沒被閹割太多,詳細差異可以看 Features by Edition。如果是要公司內部自用就放心的架吧!

安裝 ProGet

先下載 ProGet 安裝檔:下載

安裝步驟如下:
ProGet - 架設內部 NuGet Server

選擇版本,我直接選免費版,要試用企業版的人,也可以選企業版:
ProGet - 架設內部 NuGet Server

輸入註冊資訊(必填):
ProGet - 架設內部 NuGet Server

安裝路徑:
ProGet - 架設內部 NuGet Server

選擇 SQL Server 位置:

  • 如果沒有 SQL Server,選第一個 New Instance of SQL Express,它會自動幫你下載 SQL Express 及安裝。
    ProGet - 架設內部 NuGet Server
  • 如果已經有現成的 SQL Server 可以用,先建立好一個名稱為 ProGet 的資料庫,並給它資料庫的連線字串。
    ProGet - 架設內部 NuGet Server

選擇 Web Server,我在這邊是把 ProGet 架在 IIS 上面,如果沒有安裝 IIS 可以選擇有 Windows Service 的方式運行 ProGet:
ProGet - 架設內部 NuGet Server

設定 ProGet Server 運行的權限:
ProGet - 架設內部 NuGet Server

設定完成開始安裝:
ProGet - 架設內部 NuGet Server

新增 NuGet Feed

安裝好後用瀏覽器開啟 ProGet 用 Admin 登入,打開 Feeds 頁面,選擇 Create New Feed

Admin 預設帳號密碼都是 Admin
例如:安裝在本機 Prot 81 的話,開啟 URL 就是 http://localhost:81

ProGet - 架設內部 NuGet Server - Create New Feed

Feed Type 選擇 NuGet Feed,Feed Name 自訂:
ProGet - 架設內部 NuGet Server - Create New Feed

NuGet Feed 新增完成:ProGet - 架設內部 NuGet Server - Create New Feed

NuGet Feed 新增完成後,就可以透過 NuGet Push 指令把 NuGet Package 上傳到 ProGet 囉~

NuGet Package

在 Feeds 清單中,點進剛剛建立的 Feed,選擇 Add Package,就可以看到上傳 NuGet Package 的方式。
如下:

ProGet - 架設內部 NuGet Server - NuGet Package

API endpoint URL 就是 NuGet Feed 的 URL,可以透過這個 URL 上傳或下載 NuGet Package。

ProGet 有提供四種上傳 NuGet Package 的方式:

  • 從頁面上傳 *.nupkg
  • 透過 NuGet Push 指令上傳
  • 從其他 NuGet Server 同步過來
  • 從實體路徑載入

本篇以 NuGet Push 指令為主,NuGet.exe 可以到 NuGet 官網下載

打包

假設要打包 SampleLibrary 的專案,先用 Visual Studio 或 MSBuild 建置,建置完成後就可以透過 NuGet pack 指令打包 *.nupkg 檔案。指令如下:

1
NuGet.exe pack C:\SampleLibrary\SampleLibrary.csproj -Version 1.0.0.1 -Properties "Configuration=Release;OutDir=C:\SampleLibrary\SampleLibrary\bin\Release"
  • Version:要上傳到 NuGet Server 的版本不能重複。
  • OutDir:編譯後 DLL 的位置。

如果是 .NET Core 專案,用 dotnet pack 指令打包,參數可以參考官網

上傳

用 NuGet pack 打包完成後,就可以把 *.nupkg 上傳到 NuGet Server。
指令如下:

1
NuGet.exe push SampleLibrary.1.0.0.1.nupkg -ApiKey Admin:Admin -Source http://localhost:81/nuget/internal/
  • ApiKey:預設可以用 ProGet 的帳號密碼當做 NuGet ApiKey,從 ProGet 的管理中也能設定專用的 ApiKey,有興趣的可以研究看看。

在 NuGet 管理中新增 NuGet Feed,如下:

ProGet - 架設內部 NuGet Server - NuGet Package

上傳完成就可以在 NuGet 管理中,看到自製的 NuGet Package 了。

ProGet - 架設內部 NuGet Server - NuGet Package