[Day15] C# MVC LINQ常見用法 - C#&AspNetCore

在上回 [Day14] C# MVC LINQ基礎用法 - C#&AspNetCore ,我們介紹了LINQ基本用法。

而這回就要介紹LINQ常用的幾個JOIN、LEFT JOIN作法。

實作

先附上測試資料:

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
// 產品
Product[] products = new Product[] {
new Product { Id = 1 ,Name = "PS4",Price =11000 } ,
new Product { Id = 2 ,Name = "PS5",Price =15000 },
new Product { Id = 3 ,Name = "Switch",Price=9900 },
new Product{ Id= 4 ,Name="XBOX Series X",Price=15000 }
};

// 分類
ProductType[] productTypes = new ProductType[] {
new ProductType {Id = 1 ,Name = "Game"} ,
new ProductType { Id = 2 ,Name = "nintendo" } ,
new ProductType { Id =3 ,Name = "Sony" }
};


// 產品與分類關系表
Product_ProductTypeRelationship[] product_ProductTypeRelationships = new Product_ProductTypeRelationship[] {
new Product_ProductTypeRelationship {
ProductID = 1 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 1 ,ProductTypeID = 3
} ,
new Product_ProductTypeRelationship {
ProductID = 2 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 2 ,ProductTypeID = 3
},
new Product_ProductTypeRelationship {
ProductID = 3 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 3 ,ProductTypeID = 2
}
};

1.LINQ實作INNER JOIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void linqInnerJoinExample()
{
var query =
from product in products
join product_ProductTypeRelationship in product_ProductTypeRelationships on product.Id equals product_ProductTypeRelationship.ProductID
join productType in productTypes on product_ProductTypeRelationship.ProductTypeID equals productType.Id
select new
{
product.Id,
product.Name,
product.Price,
productTypeName = productType.Name
};
var result = query.ToList();
}

結果:

說明:因為是InnerJoin,都要有重疊才會關聯出,而XBOX Series X的Id沒有在product_ProductTypeRelationships裡面,所以最後沒有被顯示出來。
基本上跟原生SQL的寫法挺像的。

2.LINQ實作LEFT JOIN

寫法1:拆兩個Query

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
public void linqLeftJoinExample()
{
// product 跟 product_ProductTypeRelationship 先left join
var query1 =
from product in products
join product_ProductTypeRelationship in product_ProductTypeRelationships on product.Id equals product_ProductTypeRelationship.ProductID
into tmp
from product_ProductTypeRelationship in tmp.DefaultIfEmpty()
select new
{
product.Id,
product.Name,
product.Price,
ProductTypeID = product_ProductTypeRelationship?.ProductTypeID
};

// query1結果再跟 productTypes left join
var query2 =
from q in query1
join productType in productTypes on q.ProductTypeID equals productType.Id
into tmp
from product_ProductTypeRelationship in tmp.DefaultIfEmpty()
select new
{
q.Id,
q.Name,
q.Price,
ProductName = product_ProductTypeRelationship?.Name
};
var result = query2.ToList();
}

結果:

說明:可以得到XBOX Series X了。
因為LINQ沒有LEFT JOIN這種簡單作法,只能靠DefaultIfEmpty來操作,作法比較複雜、難直覺判斷。
((ps.不是要用來讓大家好用的查詢嗎?搞複雜了

作法2:組多個DefaultIfEmpty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void linqLeftJoinExample2()
{
var query =
from a in products
join b in product_ProductTypeRelationships on a.Id equals b.ProductID
into result1
from ab in result1.DefaultIfEmpty()
join c in productTypes on ab?.ProductTypeID equals c.Id
into result2
from abc in result2.DefaultIfEmpty()
select new{
a.Id,
a.Name,
a.Price,
ProductName = abc?.Name
} ;
var result = query.ToList();
}

結果:

說明:可以得到XBOX Series X了。
這種多個DefaultIfEmpty組法感覺蠻難讀的。
要獨立出一個值,導致得用a、b、c來組不然難知道誰是誰,就很難閱讀
且要小心有些值讀出來是空的要使用「?.」的做法,把empty的值設定為null才有辦法繼續下去,否則會執行到中間就報錯了。

以此,就是ab?.ProductTypeID,因為ab做完DefaultIfEmpty時可能ProductTypeID是為empty的,要用這ProductTypeID再去做下個join可能會出錯。

3.組成階層方式

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
public void linqLeftJoinExample3()
{

var p = (from a in products
select new ProductViewModel
{
Id = a.Id,
Name = a.Name,
Price = a.Price,
}).ToList();

foreach (var item in p)
{
var productTypeIDs = from a in product_ProductTypeRelationships
where a.ProductID == item.Id
select a.ProductTypeID;

var productTypeData = from a in productTypeIDs
join b in productTypes on a equals b.Id
select new ProductType
{
Id = b.Id,
Name = b.Name
};
item.ProductTypes = new List<ProductType>();
foreach (var x in productTypeData)
{
item.ProductTypes.Add(x);
}
}
var result = p;
string jsonData = JsonConvert.SerializeObject(result);
System.Diagnostics.Debug.WriteLine(jsonData);
}

結果:

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
[
{
"Id": 1,
"Name": "PS4",
"Price": 11000,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 3,
"Name": "Sony"
}
]
},
{
"Id": 2,
"Name": "PS5",
"Price": 15000,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 3,
"Name": "Sony"
}
]
},
{
"Id": 3,
"Name": "Switch",
"Price": 9900,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 2,
"Name": "nintendo"
}
]
},
{
"Id": 4,
"Name": "XBOX Series X",
"Price": 15000,
"ProductTypes": []
}
]

說明:用foreach搭配LINQ用組的方式去做出階層資料,並且新增ProductViewModel做最後的資料規格呈現。
這種複雜度O(n^2)不是很好,未來想到好的方式再更改。
且要注意第10行要先ToList(),執行LINQ,否則下面的foreach會造成每次初始化。
如果以前端要資料來講,這樣是最好格式,可以不用手動組。
此外,這是多對多的資料關聯,是最複雜的情況。

總結

介紹了資料常見的處理JOIN、LEFT JOIN。
覺得要處理複雜查詢用LINQ真的不太適合…,因為太難閱讀XD?。
LINQ有他強型別、Intellisense的好處,但想到有些情況要LEFT JOIN 5張時應該會蠻崩潰的。
可能就得組簡單的LINQ再搭配迴圈分步驟做,效能會差一點。

總之,在複雜的查詢我會用「原生作法搭配Dapper」或是「簡單的LINQ搭程式用迴圈做」。

更多LINQ用法可以查看:
https://docs.microsoft.com/zh-tw/dotnet/csharp/linq/

參考資料
https://dotblogs.com.tw/yc421206/2014/07/11/145907
https://ad57475747.medium.com/linq%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-6-join-%E5%A4%9A%E8%A1%A8%E5%96%AE%E5%A4%9A%E6%A2%9D%E4%BB%B6%E5%BC%8F-4076c487264f
http://vito-note.blogspot.com/2013/04/linq-3-join.html
https://docs.microsoft.com/zh-tw/dotnet/csharp/linq/perform-left-outer-joins
https://blog.csdn.net/yuanxiang01/article/details/111176654

[Day14] C# MVC LINQ基礎用法 - C#&AspNetCore

在上回 [Day13] C# MVC 驗證與授權,新刪修查按鈕權限 - C#&AspNetCore ,我們學會了進階的權限切割。

而這回就要開始操作資料了,使用LINQ。

LINQ

Language Integrated Query (LINQ) 是一組以直接將查詢功能整合至 C# 語言為基礎之技術的名稱。 傳統上,資料查詢是以簡單的字串表示,既不會在編譯時進行類型檢查,也不支援 IntelliSense。 此外,您還必須針對每種資料來源類型學習不同的查詢語言:SQL 資料庫、XML 文件、各種 Web 服務等等。透過 LINQ,查詢會是第一級語言建構,和類別、方法及事件相同。 您可以使用語言關鍵字和熟悉的運算子,針對強型別的物件集合撰寫查詢。 LINQ 技術系列會針對物件 (LINQ to Object)、關聯式資料庫 (LINQ to SQL) 與 XML (LINQ to XML),提供一致的查詢體驗。

對於撰寫查詢的開發人員來說,LINQ 最明顯的「語言整合」部分就是查詢運算式。 查詢運算式是以宣告式「查詢語法」撰寫。 透過使用查詢語法,您就可以利用最少的程式碼,針對資料來源執行篩選、排序及分組作業。 您可以使用相同的基本查詢運算式模式來查詢和轉換 SQL 資料庫、ADO.NET 資料集、XML 檔和資料流程和 .net 集合中的資料。

您可以使用 C# 針對下列項目撰寫 LINQ 查詢:SQL Server 資料庫、XML 文件、ADO.NET 資料集,以及支援 IEnumerable 或泛型 IEnumerable 介面的任何物件集合。 也有協力廠商針對許多 Web 服務和其他資料庫實作提供 LINQ 支援。

所有 LINQ 查詢作業都包含三個不同的動作:

  1. 取得資料來源。
  2. 建立查詢。
  3. 執行查詢。

實作

HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public IActionResult Index()
{
// Specify the data source.
int[] scores = new int[] { 97, 92, 81, 60 };

// Define the query expression.
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;

// Execute the query.
foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
return View();
}

說明:
以上就是過濾>80的分數。

  • IEnumerable:第7行,公開能逐一查看非泛型集合內容一次的列舉程式。而其實可以用var當作宣告替代。
  • scoreQuery:過濾的結果。 下圖顯示完整的查詢作業。

總結

本人習慣直接操作SQL語言,但LINQ說實在也不錯(可以直接防SQL INJECTION、強型別的優勢)。
但感覺上LINQ比較適合簡單的查詢,變成在程式裏面爬出來後去操作,性能可能會低,但比較穩。
而對於複雜的SQL查詢還是有困難QQ,變成翻譯家的感覺。
基於以上原因,複雜查詢我應該會使用Dapper來做,寫原生。

此外,以有寫過Laravel跟Java的經驗來說,LINQ是C#微軟Only。
也就表示熟悉LINQ可能會有換其他語言SQL卡住、忘記正常該怎麼組的問題。

在這篇中先小試一下,下篇將注重一些平常可能會做的LINQ查詢操作。

參考資料
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/linq/
https://ithelp.ithome.com.tw/articles/10194468
https://blog.darkthread.net/blog/linq-or-direct-sql/

後端API適合共用APP與WEB嗎?

先說結論:不建議。

因為會出現以下問題:

  1. 開發時程問題:因網站改API,APP就必須要更新API。
  2. 難同時開發:可能先做完WEB才會做APP,或反過來,彼此資料也差異大。
  3. 難同時滿足兩者:畫面差異大,後端不知道APP或WEB要底要哪種基礎資料,可能少或多給。
  4. 討論時間拉長:因為第3點肯定會延長開會討論,也延遲到開發。

此外,如果APP要向下支援,不強制更新,那會需要API版號,如v1、v2…等,可以塞在header裡面。
而向下支援可能就會有資料庫table異動與欄位被刪,像是沒辦法新增、編輯,這時僅能提示使用者安裝新版本了。
如果v1版要跟v100版間要相容肯定會處理的蠻崩潰的。

基於以上理由,WEB、APP分兩組API是比較好長期維護、開發的方法。

最後不推graphQL,個人工作經驗只會讓專案變得難以維護。
然後API一定要RESTful嗎?
這也不一定,有時分權而難以限定權限,就會有漏洞。反而增加開發風險。
雖然有人會覺得RESTful就是主流,潮。但然後呢…。Restful很簡潔,能讓知道這模式的人容易上手,但是否必要要看開發者所在的環境而定。
可參考以下參考資料,有篇提到KISS (Keep It Simple, Stupid)。

參考資料
https://www.ptt.cc/bbs/Soft_Job/M.1624876117.A.086.html
https://blog.darkthread.net/blog/is-restful-required/

[Day13] C# MVC 驗證與授權,新刪修查按鈕權限 - C#&AspNetCore

在上回 [Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore ,我們介紹了登入與登出。

而這回,將探討的是驗證與授權的細部權限,即所謂的新刪修查權限。
如:某角色是禁止使用特定功能的編輯。

我在之前也有了打篇與這相關的資料表規劃 : 資訊系統的角色權限規劃,含資料表設計

授權

授權是指決定使用者能夠做什麼的處理常式。 例如,系統管理使用者可以建立文件庫、新增檔、編輯檔,以及將它們刪除。

實作

目標,可以讓一個有權限的使用者依照他的權限去限制:檢視、新增、刪除、編輯。

權限代碼:

  • Basic_UserManagement_View
  • Basic_UserManagement_Create
  • Basic_UserManagement_Modify
  • Basic_UserManagement_Delete

使用者擁有的權限代碼:

  • Basic_UserManagement_View
  • Basic_UserManagement_Create
  • Basic_UserManagement_Delete

所以該使用者只能檢視、建立、刪除,並且無法編輯。

在此先列出檔案目錄,以及異動檔案(黃色標記處):

1.首先先建立權限代碼的定義檔案
~/Authorization/Permissions.cs

1
2
3
4
5
6
7
8
9
10
11
12
namespace CookieAuthentication.Authorization
{
// 權限代碼
public static class Permissions
{
// 基本資料管理-人員管理
public const string Basic_UserManagement_View = "Basic_UserManagement_View";
public const string Basic_UserManagement_Create= "Basic_UserManagement_Create";
public const string Basic_UserManagement_Modify = "Basic_UserManagement_Modify";
public const string Basic_UserManagement_Delete = "Basic_UserManagement_Delete";
}
}

說明:
未來如果要新增各種功能代碼都可以從這新增。

2.建立權限的AuthorizationRequirement,要求規格
~/Authorization/PermissionAuthorizationRequirement.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
using Microsoft.AspNetCore.Authorization;
namespace CookieAuthentication.Authorization
{
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public string[] Permissions { get; set; }

public PermissionAuthorizationRequirement(string[] permissions)
{
Permissions = permissions;
}
}
}

說明:
為了要能夠自訂權限的邏輯,所以需要一個IAuthorizationRequirement幫我們傳遞授權資料的格式。

3.建立權限邏輯的控制器

~/Authorization/PermissionAuthorizationHandler.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
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CookieAuthentication.Authorization
{
// 權限控制邏輯
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
// 如果沒有登入直接返回失敗
var accountClaim = context.User.FindFirst(x => x.Type == "Account");
if (accountClaim == null)
{
context.Fail();
return Task.CompletedTask;
}

// 要求的權限
var requirementPermissions = requirement.Permissions;

// 取得使用者的權限, 這段可以從資料庫撈,在這範例用陣列
List<string> userPermission = new List<string>();
userPermission.Add(Permissions.Basic_UserManagement_View);
userPermission.Add(Permissions.Basic_UserManagement_Delete);
userPermission.Add(Permissions.Basic_UserManagement_Modify);

// 檢查是否使用者具備權限
bool isExist = false;

for (int i = 0; i < userPermission.Count(); i++)
{
for (int j = 0; j < requirementPermissions.Count(); j++)
{
if (userPermission[i] == requirementPermissions[j])
{
isExist = true;
break;
}
}
}
// 如果具備則授予成功
if (isExist)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
}

說明:
在此繼承了AuthorizationHandler,是為了要撰寫自己的判斷邏輯。
主要在HandleRequirementAsync裡面檢查傳進來的權限與使用者是否匹配,如果是就設定context.Succeed(requirement);不是就設定context.Fail()。並在最後回傳return Task.CompletedTask。
在22~48是關鍵,在這如果連結資料庫就從資料庫抓使用者的權限表來比對即可。
我在24~28先用假資料替代資料庫撈出來的情況。

4.建立過濾器Filter,之後要用來套用在Controller的Action上

~/Authorization/PermissionFilter.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.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace CookieAuthentication.Authorization
{
public class PermissionFilter:Attribute, IAsyncAuthorizationFilter
{
public string[] permissions { get; set; }

public PermissionFilter(params string[] permissions)
{
this.permissions = permissions;
}

public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new PermissionAuthorizationRequirement(permissions));
if (!authorizationResult.Succeeded)
{
// 如果授權失敗,設定為未授權
context.Result = new UnauthorizedResult();
}
}
}
}

說明:
重點在21~27,使用authorizationService.AuthorizeAsync去認證權限,就會把資料傳到我們上面建立的PermissionAuthorizationHandler.cs裡面判斷。
如果最後判斷沒權限則設定context.Result = new UnauthorizedResult();表示禁止。

5.增加過濾器服務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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using CookieAuthentication.Authorization;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
namespace CookieAuthentication
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// 註冊需求和處理常式,套用自訂權限控制器
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

// 從appsettings.json讀取登入逾時設定
double LoginExpireMinute = this.Configuration.GetValue<double>("LoginExpireMinute");

// 建立驗證中介軟體服務
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
// 登入逾期設定,如果沒給預設14天
option.ExpireTimeSpan = TimeSpan.FromMinutes(LoginExpireMinute);
// 限制cookie不能延期
option.SlidingExpiration = false;
});

services.AddControllersWithViews(options => {
//CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller就不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}

// 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.UseAuthentication(); // 啟用身份驗證
app.UseAuthorization(); // 啟用授權,指的是Controller、Action可加上驗證 [Authorize] 屬性


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

說明
只有在26行,增加一行,表示套用驗證邏輯的PermissionAuthorizationHandler

1
2
// 註冊需求和處理常式,套用自訂權限控制器
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

6.限制頁面上的檢視權限,在Controller增加權限過濾器Filter

~/Controllers/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PermissionFilter(Permissions.Basic_UserManagement_View)]
public IActionResult Privacy()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<ul>");

foreach (Claim claim in HttpContext.User.Claims)
{
sb.AppendLine($@"<li> claim.Type:{claim.Type} , claim.Value:{ claim.Value}</li>");
}
sb.AppendLine("</ul>");

ViewBag.msg = sb.ToString();
return View();
}

說明:
添加
[PermissionFilter(Permissions.Basic_UserManagement_View)]
表示允許某權限的代碼進入,在此就是Permissions.Basic_UserManagement_View,表示畫面的檢視權限。

7.限制頁面上的按鈕權限

~/View/Home/Privacy.cshtml

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
@using Microsoft.AspNetCore.Authorization
@using CookieAuthentication.Authorization

@inject IAuthorizationService AuthorizationService
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>

<!-- 按鈕區塊 -->
@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Create }))).Succeeded)
{
<button type="submit">新增</button>
}

@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Modify }))).Succeeded)
{
<button type="submit">編輯</button>
}

@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Delete }))).Succeeded)
{
<button type="submit">刪除</button>
}
<!-- ./按鈕區塊 -->

<!--登出區塊-->
<div>
您的登入資訊↓
</div>
<div>
@Html.Raw(ViewBag.msg)
</div>
<div>
<a href="@Url.Action("Logout","Home")">登出</a>
</div>
<!--./登出區塊-->

說明:
在13~27為限制擁有某些權限的代碼才啟用
舉例:
@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Modify }))).Succeeded)

8.完成!
該使用者登入後只有:畫面檢視、刪除、新增權限,而沒有編輯權限。
可以見到成功進Privacy頁面,而只有新增、刪除按鈕有顯示,編輯按鈕則因為權限限制沒有被顯示出來。

總結

如此一來權限問題也難不倒了@@
而接下來主要以資料呈現為主題,所以下回是LINQ的資料操作。

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/views?view=aspnetcore-3.1
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1

[Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore

在上回 [Day11] C# MVC傳遞參數的方式 - C#&AspNetCore ,我們介紹了頁面與頁面間的參數傳遞。

而這回,將探討的是驗證與授權,並以實作登入、登出為示範。

此外,對於權限.net core MVC本身就有提供可實作的方法,連結如下:
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1

驗證與授權

步驟:驗證->授權

  • 驗證 (Authentication):讓系統認得你是誰
    • Cookie-Based驗證
    • Token-Based驗證
    • Idetity 驗證
    • OAuth2 驗證
    • Windows 驗證
  • 授權 (Authorization):讓系統判斷你是否有權限
    • AuthorizeAttribute授權

驗證有很多種沒有好壞之分,以我觀點來說就是:「用官方範例並符合需求的簡單作法」
因為這樣能避免維護問題、要改動也容易些。
在這網頁上是.net core MVC,所以最簡單實作方式就是「Cookie-Based驗證」,未來要使用API時也只要增加「Token-Based驗證」即可。

使用瀏覽器的 Cookie 儲存使用者驗證資訊。
要使用Cookie驗證要小心的是資料不能太多,因為瀏覽器有4096 bytes限制。
(如果是一些舊瀏覽器就會遇到問題,所以要注意資料就是存識別資料即可)
此外,ASP.NET Core Cookie驗證,在前台會以加密方式儲存,避免使用者去修改,也有自動逾期時間,整體上來安全程度是有的。

實作

主要參考 使用 cookie 驗證但不使用 ASP.NET Core Identity[ASP.net Core] 實作 Cookie based 登入機制 這兩篇進行實作,並以7個步驟完成

先列出主要異動的檔案:
(黃色標記處為異動檔案)

1.先增加登入逾期的時間定義值

appsettings.json

1
2
3
4
5
6
7
8
9
10
11
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"LoginExpireMinute": 60, //登入逾期時間
"AllowedHosts": "*"
}
  • 第9:LoginExpireMinute表示幾分後逾期登出。

2.到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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

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

public IConfiguration Configuration { get; }

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

// 從appsettings.json讀取登入逾時設定
double LoginExpireMinute = this.Configuration.GetValue<double>("LoginExpireMinute");

// 建立驗證中介軟體服務
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
// 登入逾期設定,如果沒給預設14天
option.ExpireTimeSpan = TimeSpan.FromMinutes(LoginExpireMinute);
// 限制cookie不能延期
option.SlidingExpiration = false;
});

services.AddControllersWithViews(options => {
// CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller就不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}

// 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.UseAuthentication(); // 啟用身份驗證
app.UseAuthorization(); // 啟用授權,指的是Controller、Action可加上驗證 [Authorize] 屬性


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

主要改以下幾點:

  • [26~45]:ConfigureServices追加服務,先取得登入逾期的時間參數LoginExpireMinute。之後追加AddAuthentication服務,並修改AddControllersWithViews內容。
  • [65~66]:啟用身份驗證、授權

3.再來是控制器的部分,主要設定登入、登出、檢視登入後資訊的方能

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
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using CookieAuthentication.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace CookieAuthentication.Controllers
{

public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

[Authorize]
public IActionResult Privacy()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<ul>");

foreach (Claim claim in HttpContext.User.Claims)
{
sb.AppendLine($@"<li> claim.Type:{claim.Type} , claim.Value:{ claim.Value}</li>");
}
sb.AppendLine("</ul>");

ViewBag.msg = sb.ToString();
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}


[HttpPost]
public async Task<IActionResult> Login(string Account, string Password,bool IsRememberMe)
{

if ((Account == "test01" && Password == "test01pwd") == false)
{
ViewBag.errMsg = "帳號或密碼輸入錯誤";
return View("~/Views/Home/Index.cshtml"); // 登入失敗導回頁面
}


// 登入成功,建立驗證 cookie
Claim[] claims = new[] { new Claim("Account", Account) }; // Key取名"Account",方便取出後可以對資料庫做操作
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

// 呼叫 SignInAsync 以登入使用者
await HttpContext.SignInAsync(claimsPrincipal,
new AuthenticationProperties()
{
//IsPersistent = false:瀏覽器關閉立馬登出;IsPersistent = true 就變成常見的Remember Me功能
IsPersistent = IsRememberMe,
});

// 導至隱私頁面
return RedirectToAction("Privacy", "Home");
}


public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToAction("Index", "Home");// 導至登入頁
}
}
}
  • 登入:在56~82,主要是檢查帳密>建立Cookie權杖>在使用SignInAsync以Cookie權杖做登入。
  • 登出:在85~89,很簡單的一行await HttpContext.SignOutAsync();即可。
  • 檢視登入後資訊:在33~47,[Authorize]表示授權才能啟用,主要把登入後的資訊從HttpContext中取出給前端顯示。

5.之後建立登入頁,我這把登入做在Index.cshtml

Index.cshtml

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
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<!--登入區塊-->
@using (Html.BeginForm("Login", "Home", new { ReturnUrl = Context.Request.Query["ReturnUrl"] }, FormMethod.Post, true, new { name = "loginForm", autocomplete = "off" }))
{
<div>
<label>帳號:</label>@Html.TextBox("Account")
</div>
<div>
<label>密碼:</label>@Html.Password("Password")
</div>
<div>
<label>記住我:</label>@Html.CheckBox("IsRememberMe")
</div>
<div>
<button type="submit">提交</button>
</div>
<div style="color:red;">
<!--顯示登入失敗訊息 -->
@ViewBag.errMsg
</div>
}
<!--./登入區塊-->

6.登入後才可用的頁面,我這把Privacy.cshtml當成是要登入才能進的頁面

Privacy.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>

<!--登出區塊-->
<div>
您的登入資訊↓
</div>
<div>
@Html.Raw(ViewBag.msg)
</div>
<div>
<a href="@Url.Action("Logout","Home")">登出</a>
</div>
<!--./登出區塊-->
  • 第16:實作登出,點下去回連接到HomeController.cs的Logout動作

7.完成,看看登入、登出效果。

總結

以這種Cookie-Based驗證方式不用管是DB First還是Code First都可以用,只要簡單做個設定即可!
如果是用Idenity在資料表部分會要符合特定格式,可能對某些已成形的系統不好加入。

最後,在這篇裡面實現了登入、登出,但現在的網站應該還有更細的權限,如:特定功能的新刪修查、某按鈕權限、只能看不能編輯。
因此,在下篇中將會實作更細部分的權限分則。
即以角色為基礎的存取控制(Role-based access control,RBAC)。

(( 這篇文好長

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1
https://ithelp.ithome.com.tw/articles/10249930
https://blogger.tigernaxo.com/post/dotnetcore31/auth/auth_guild_3/
https://ithelp.ithome.com.tw/articles/10199102
https://ithelp.ithome.com.tw/articles/10198150
https://blog.miniasp.com/post/2019/12/25/asp-net-core-3-cookie-based-authentication
https://dotblogs.com.tw/shadow/2019/01/16/105615
https://docs.microsoft.com/zh-tw/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1

[Day11] C# MVC傳遞參數的方式 - C#&AspNetCore

在上回 [Day10] C# MVC 檢視元件View Compoment使用- C#&AspNetCore ,我們介紹了View Component的做法,算是把整個MVC的View摸透了。

而這回則是要探討如何在這些頁面間傳遞參數。

Model Binder

主要會透過Model Binder來完成傳遞,Model Binder主要是用於將HTTP request資料對應到controller/Action去。
參數可能是string、integer、class、list、array…等

而為什麼能接到呢?就是前面的這些設定

Startup.cs

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

常見的傳遞參數方法

  1. 從網址傳遞

例:https://localhost:44378/Home?id=1

1
2
3
4
5
public IActionResult Index(int id)
{
Console.Write(id);
return View();
}

後面帶參數即可,以此是id,有多少個帶多少個。

  1. 從form post請求

View頁面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form asp-action="Create">
<div>
<label asp-for="Name"></label>
<input asp-for="Name" />
<span asp-validation-for="Name"></span>
</div>
<div>
<label asp-for="Age"></label>
<input asp-for="Age" />
<span asp-validation-for="Age"></span>
</div>
<div>
<input type="submit" value="Save" />
</div>
</form>

Controller頁面:

1
2
3
4
5
6
7
8
9
10
11
12
[HttpPost]
public IActionResult Create(UsersModel usersmodel)
{
if (ModelState.IsValid)
{
//如果連結資料庫在這進行儲存

//導回首頁
return RedirectToAction(nameof(Index));
}
return View(usersmodel);
}

基本上就是有特別多資料或固定規格會使用Model去接,不然可以參考以下補充資料。

補充

通常上面的就足夠來撰寫網頁了,但偶而還是會需要取得請求的特定資料
這時還有以下方法可以用:
Header、Request、Query、Form、Route、FormBody

總結

雖然有時會因為避免用戶改資料,把參數傳遞在session storage就是了。
但缺點是他是前台Javascript,要在前台追參數。
最後,當專案大起來時勢必要注意參數的管理,不然A到B頁可能會有各種傳法,在追參數、修改上就會花比較多時間。

預計下回會稍微探討權限部分。

參考資料
https://ithelp.ithome.com.tw/articles/10240381

在網路中減少自己觸及的資訊量

2021年的今天,似乎什麼事都要用透過網路。
但每天都接觸網路上的資訊就會讓人覺得煩躁、被影響。
於是我開始想,有沒有什麼方法”減少資訊量”呢?
因為現在已不可能100%離開網路,像是在台灣工作可能還是要Line。

以下是我想到的作法:

  1. 減少用社群軟體頻率,如7天1次1小時
  2. 不看新聞、論壇
  3. 不看Youtube
  4. 手機換功能型手機
  5. 少用Gxxxxgle搜尋
  6. 瀏覽器ADBLOCK過濾廣告
  7. 轉移注意力到離線的東西

其實,第2~6點都可以靠自己戒掉
只有第1點是現代是必須的,不然就沒朋友了,就是把它當做訊息軟體用,不看動態也不發文。
第4點,則是我們要自己培養的見解能力,不要什麼都靠Gxxxxgle,手動去翻閱實體書或是自己想辦法,十年前的自己可以這樣,現在也一定可以。就是找一套自己離線方法。

然後,可能會覺得不用網路就不能學習,那我問用網路又學到什麼實際能工作的技能…,真正派得上用場的我認為都是內化在自己腦中的知識。

最後,希望可以主動抵制,不要再被演算法強迫推銷了!
一些不相干的事在我們周遭太多了。
關心自己周遭實際碰過的人事物比較重要。

[Day10] C# MVC 檢視元件View Compoment使用- C#&AspNetCore

在上回 [Day9] C# MVC View使用 - C#&AspNetCore ,我們介紹了View的操作及用法,並稍微帶了Partial View。
而這回,我們將介紹檢視元件View Compoment。

因為原本MVC5的@HTML.action在.net core MVC中已經被移除掉了,造成Partial View會無法綁後端Controller撰寫商業邏輯。
因此,要改用View Compoment的做法。

View Compoment

檢視元件與部分檢視類似,但功能更強大。 檢視元件不會使用模型繫結(看你要不要都可以),並且只取決於呼叫它時所提供的資料。

檢視元件:

  • 轉譯區塊,而不是整個回應。
  • 包含控制器與檢視之間的相同關注點分離和可測試性優點。
  • 可以有參數和商務邏輯。
  • 它通常是從配置頁面叫用。
  • 不參與控制器生命週期,無法使用Filter。
  • 非 HTTP 端點的呼叫,不處理Http Request/Response ,ViewComponent是透過程式碼叫用 。

簡言之,可以撰寫商務邏輯。
有一個專門負責這View Component商務邏輯的程式碼。而你在View中也能把參數傳進View Component裡面。

View Compoment建立與使用

照微軟官方文件,View Compoment有限定的資料夾結構,避免維護困難。
黃色標記是等等會新增的檔案。

  • ViewComponents資料夾必須在根目錄下新增這資料夾名稱才可以使用ViewComponents,否則底下的程式碼會出現錯誤。
    • ShowUserInfo.cs是以View Component為名稱命名的業務邏輯管理器。
  • Shared/Components資料夾底下都是以資料夾為名的Component名稱。
    • ShowUserInfo資料夾為View Component名稱,底下有一個Default.cshtml,為Component的視圖。

1.首先先建立業務邏輯的檔案

ShowUserInfo.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.ViewComponents
{
public class ShowUserInfo : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(string number, string name,string tel)
{
Customer customer = new Customer();
customer.Name = name;
customer.Number = number;
customer.Tel = tel;
return View(customer);
}
}
}
  • IViewComponentResult這段回應照寫就好,算是固定寫法。

在13~19行可以用Model、ViewBag、ViewData,簡單來說就是當你在寫Controller裡的action那樣。
我這邊為了求謹慎,所以回傳的是個強型別Model,因為固定規格比較好維護。

2.再來是View Component的視圖

ShowUserInfo.cshtml

1
2
3
4
5
6
7
8
@model WebApplication1.Models.Customer;

<ul>
<li>Number:@Model.Number</li>
<li>Name:@Model.Name</li>
<li>Tel:@Model.Tel</li>
</ul>

在此就把傳過來的資料顯示即可。

3.實際把View Component套用到View上。

Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@model WebApplication1.Models.Customer;
@using WebApplication1.ViewComponents;
@{
ViewData["Title"] = "Home Page";
}
<ul>
<li>@Model.Number</li>
<li>@Model.Name</li>
<li>@Model.Tel</li>
</ul>
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

// 呼叫View Component
@await Component.InvokeAsync(nameof(ShowUserInfo), new { number = "A007", name = "YS",tel="00-00000000" })

  • @await Component.InvokeAsync用這方法傳遞參數來呼叫View Component顯示。

如此一來就完成了,在網頁上呈現。

總結

整體流程大概是這樣:

  1. 使用者進到View頁面
  2. Controller接受並回應資料給View
  3. View上的Razor語法呼叫ViewComponent
  4. 把參數再丟給ViewComponent處理
  5. 之後結果再回傳給整個View。

感覺這種方式真是爽快,功能又強,減少了很多Controller要顧慮的生命週期。
有獨立區塊用View Component會比用Partial View輕鬆很多。(時代的眼淚 誤~*

參考資料
https://dotblogs.com.tw/shadow/2021/04/10/174425
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-5.0

[Day9] C# MVC View使用 - C#&AspNetCore

在上回 [Day8] C# MVC Controller控制器使用 - C#&AspNetCore ,我們介紹了Controller的基本用法。

而這回就是使用者畫面了,也就是MVC的View

在這篇中會介紹:

  1. View的Razor語法
  2. 可共用的View 部分檢視Partial View

View的Razor語法

通常在View中看到@出現通常就是Razor語法了。
基本上就是印出值,再來就是一些特定用法。

  • 後端註解
    1
    @*註解*@
  • 輸出@符號
    1
    @@
  • @變數名稱,取值
    1
    @DateTime.Now
  • 陳述式
    1
    @(5*3)
  • 程式碼區塊
    1
    @{ }
  • for用法
    1
    2
    3
    4
    5
    6
    @for (int i = 0; i < 10; i++)
    {
    <text>
    word
    </text>
    }
  • foreach用法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @{
    string[] fruits = { "apple", "orange", "banana" };
    <ul>
    @foreach (var item in fruits)
    {
    <li>@item</li>
    }
    </ul>
    }
  • 變數
    1
    2
    3
    4
    @{
    var name = "mvc";
    }
    <p>@name 您好</p>

Partial View 部分檢視

在網頁時常會有需要共用的畫面,如側邊欄、頂端橫幅等。
不想要在每個頁面都寫一個,這時就會使用到Partial View把他們切出去。

Partial View前面會有個_底線
可以看到預設建立的MVC專案就有不少Partial View了。

_ViewStart.cshtml
1
2
3
@{
Layout = "_Layout";
}

Layout的進入點_ViewStart.cshtml在此可以選擇預設要套用的版,如一些網站會有前台、後台之分。
如果想把View切成前後台可以參考這篇動態切換適配的Layout:
https://dotblogs.com.tw/stanley14/2016/06/29/202231
其實就是在_ViewStart.cshtml這檔案做些適配而已

再來我們看_Layout.cs,版面前台主View

_Layout.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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2021 - WebApplication1 - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>
  • @RenderSection("Scripts", required: false)用來輸出主板中區段的內容。required是指此Section是否一定要有資料顯示,如果設成true時,檢視中沒有定義此Section的值,那網頁就會掛掉。
    子版則可以將下列區段內容輸出到主板
    1
    2
    3
    @section Scripts {
    <script type="text/javascript" src="~/scripts/main.js"></script>
    }
  • @RenderBody()所有被導向主版的view會被載入到@RenderBody

切Partial的方法

我們可以發現_Layout.cs有段nav區塊可以共用,把他切出去吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>

之後在_Layout將他引進

_Layout.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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
@await Html.PartialAsync("_Nav")
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2021 - WebApplication1 - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>
  • @await Html.PartialAsync("_Nav") 表示用異步的方式把Partial區塊引進來。使用 HTML 協助程式時,最佳做法是使用 PartialAsync。 PartialAsync 會傳回包裝在 Task 的 IHtmlContent 類型。 方法的參考方式,是在等候的呼叫前面加上 @ 字元。此外,目前以不建議使用同步方式載入了,因為官方說會造成死結。

如此以來就會更專注在每個小區塊。

這時可能有人會問,Partial沒Controller怎麼載資料呢?所以要介紹另個載入的方式。

Partial傳遞資料的方式

我們查看@await Html.PartialAsync("_Nav")的函式多載,可以發現能透過Model方式將資料傳入。

我們建立一個ViewModel叫NavViewModel

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
public class NavViewModel
{
public string BrandName { get; set; }
}
}

將原本的PartialAsync傳入NavViewModel

_Layout.cshtml
1
@await Html.PartialAsync("_Nav",new NavViewModel() { BrandName = "WebApplication1" })

之後在_Nav.cshtml顯示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@model WebApplication1.Models.NavViewModel;
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">
@Model.BrandName
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
  • @Model.BrandName即可印出Model資料

因為會拉出去獨立寫的通常都是固定規格,所以建議用Model,而不是用弱型別的ViewData、ViewBag方式。

總結

MVC的View真的做到很嚴謹,讓整體資料有固定的方式作呈現,關鍵就在那一個View只能綁一個Model,讓資料減少亂竄的可能性。
而在Partial也是可以透過API方式去自行載入資料。

預計下篇可能還是View,會探討一些常見作法在拆頁面結構上。

參考資料
https://ithelp.ithome.com.tw/articles/10160185
https://sites.google.com/site/yuedumvcbiji/4-view/layout-cshtml
https://docs.microsoft.com/zh-tw/aspnet/core/mvc/views/layout?view=aspnetcore-5.0
https://docs.microsoft.com/zh-tw/aspnet/core/mvc/views/partial?view=aspnetcore-5.0
https://ithelp.ithome.com.tw/articles/10186517
https://ithelp.ithome.com.tw/articles/10205881
https://ithelp.ithome.com.tw/articles/10240368
https://dotblogs.com.tw/stanley14/2016/06/29/202231

[Day8] C# MVC Controller控制器使用 - C#&AspNetCore

在上回 [Day7] C# MVC Model模型連結資料庫 - C#&AspNetCore ,我們把Model連上了資料庫,算是把Model的基礎流程跑完了。

這回就來看Controller吧!

在這篇中主要介紹Controller把資訊傳送給View,有以下方法

  1. ViewData
  2. ViewBag
  3. 多個Model傳給View

控制器(Controller)

負責轉發請求,對請求進行處理。
當使用者進到了前台的視圖(View),對畫面操作所觸發的事件就會跑到控制器。
在控制器裡負責拿到模型(Model)的資料後加上些業務邏輯後返視圖(View)。
負責處理主要商業邏輯處理,資料格式處理,大部分介於Model 與 View 之間,處理資料流的溝通。

建立Controller

1.在Controller資料夾Add > Controller

2.選Empty

3.MVC Controller Empty

4.接下來會產生程式碼,我們增加一行ViewData的程式碼

TestController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Controllers
{
public class TestController : Controller
{
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";
return View();
}
}
}

  • IActionResult:表示動作結果,即使用Post([HttpPost])或Get( [HttpGet])時會使用。
  • ViewData:表示在該View可以使用的Data,為弱型別。通常我們會多建立一個ViewModel作為強型別用,因為可以在編譯時就知道錯誤,上線後、維護也比較穩。ViewData算是有點偷懶的傳資料方式。

接下來我們添加View畫面,Test>Index.cshtml

Index.cshtml
1
2
3
4
5
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]

如此一來就能把資料帶進來了

補充Result種類

JsonResult:用來單純回傳JSON

1
2
3
4
public JsonResult TestJsonResult()
{
return Json(new { data = 5 });
}

結果:

FileResult:用來回傳檔案

1
2
3
4
5
public FileResult TestFileResult()
{
string excelPath = @"C:\example.xlsx";
return File(excelPath, "application/vnd.ms-excel", "example.xlsx");
}

Controller傳多個Model進View的方法

最好作法就是強型別的ViewModel了。

我們建立一個Model,裡面宣告Model類別屬性

TestViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
public class TestViewModel
{
Customer Customer { get; set; }
public List<Product> Products { get; set; }

}
}

TestController.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
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
public class TestController : Controller
{
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";

TestViewModel testViewModel = new TestViewModel();
testViewModel.Customer = new Customer() ;
testViewModel.Customer.Name = "YU HSIANG";
testViewModel.Customer.Number = "A001";
testViewModel.Customer.Tel = "00-00000000";

testViewModel.Products = new List<Product>();
testViewModel.Products.Add(new Product() { Name = "PS4", Number = "P001", Price = 11000 });
testViewModel.Products.Add(new Product() { Name = "PS5", Number = "P002", Price = 15000 });

return View(testViewModel);
}
public JsonResult TestJsonResult()
{
return Json(new { data = 5 });
}


public FileResult TestFileResult()
{
string excelPath = @"C:\example.xlsx";
return File(excelPath, "application/vnd.ms-excel", "example.xlsx");
}
}
}
Index.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@model WebApplication1.Models.TestViewModel
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]<br>
@Model.Customer.Name<br>
@Model.Customer.Number<br>
@Model.Customer.Tel<br>

<table>
@foreach (Product item in Model.Products)
{
<tr>
<td>@Html.TextBoxFor(m => item.Number)</td>
<td>@Html.TextBoxFor(m => item.Name)</td>
<td>@Html.TextBoxFor(m => item.Price)</td>
</tr>
}
</table>

成功呈現多個Model,並且又是強型別

額外的方式傳值Viewbag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";

//使用ViewBag
ViewBag.msg = "使用ViewBag的資料";

TestViewModel testViewModel = new TestViewModel();
testViewModel.Customer = new Customer() ;
testViewModel.Customer.Name = "YU HSIANG";
testViewModel.Customer.Number = "A001";
testViewModel.Customer.Tel = "00-00000000";

testViewModel.Products = new List<Product>();
testViewModel.Products.Add(new Product() { Name = "PS4", Number = "P001", Price = 11000 });
testViewModel.Products.Add(new Product() { Name = "PS5", Number = "P002", Price = 15000 });


return View(testViewModel);
}
Index.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@model WebApplication1.Models.TestViewModel
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]<br>
@ViewBag.msg<br>
@Model.Customer.Name
<br>
@Model.Customer.Number
<br>
@Model.Customer.Tel
<br>

<table>
@foreach (Product item in Model.Products)
{
<tr>
<td>@Html.TextBoxFor(m => item.Number)</td>
<td>@Html.TextBoxFor(m => item.Name)</td>
<td>@Html.TextBoxFor(m => item.Price)</td>
</tr>
}
</table>

結果:

用ViewBag跟ViewData缺點都在弱型別,難以在編譯時就知道錯誤存在。

總結

從這篇中可以知道如何透過Controller傳資料給View。
在下篇預計就是介紹View了。

參考資料
https://ithelp.ithome.com.tw/articles/10240345
https://docs.microsoft.com/zh-tw/aspnet/core/web-api/action-return-types?view=aspnetcore-5.0