0%

C# 教育訓練 using Visual Studio Code

終於開始今年的技術性教育訓練了,有了新的學習標的總是讓我很興奮,有幸王智民老師有了精闢的講解,收穫良多。

我是老派的軟體工程師,也因為工作環境,文書編輯器一直是必備的工具,有時需要用到 GUI 介面的開發工具光安裝就讓人頭痛。 Oracle SQL Developer 如非必要總不會打開,至今 Oracle SQL Developer 對我還是很陌生,SQL Plus 是我的最愛。

安裝一個 Visual Studio 需要 20-50 GB 的可用空間,喔! 也許有人的硬碟就爆掉了,在 Windows 環境 C 硬碟千萬要留大一些,如果可以選擇就不要將軟體工具裝在 C 槽。這裡是使用 Visual Studio Code 開發 .NET 專案的簡介,希望能對各位有些幫助。

這裡就用 Visual Studio Code 將王智民老師第一堂課的程式碼走一次,但我使用的是較新的版本 .NET Core 3.1。因為 .NET Core 2.2 已於去年就 EOL 了。

安裝

首先要安裝 Visual Studio Code.Net Code SDK。 記得要安裝 SDK 版。

這裡有一些快速指引 Visual Studio Code Working with C#,最好也把 VS Code 的兩個 C# Extensions 安裝起來:

安裝完成後,開啟 VSCode,打開一個終端視窗或命令提示字元輸入:

1
dotnet --version

如果一卻正常會返回 .NET Code 的版本訊息,那我們就可以開始了。

往後我們常會用到 donnet 指令,例如建立解決方案、建立專案、安裝 NuGet Packages、或顯示本機的一些 .NET 設定例如:

1
dotnet nuget locals all --list

可以顯示本機安裝 NuGet Packages 的目錄。

解決方案與專案設定

我們先在電腦裡面用一個空的資料夾建立 TrainSample 的 .NET 解決方案。然後使用 VSCode 打開解決方案資料夾 TrainSample,開啟一個終端視窗,我們需要用 dotnet 初始化解決方案,確定位於 TrainSample 目錄中:

1
dotnet new sln

範本 “Solution File” 建立成功後,目錄下會產生一個 TrainSample.sln 檔案。

接著我們要在這個解決方案目錄下產生專案 MyWeb。你可以使用 dotnet new –help 查詢 SDK 所提供的各類專案的基本初始範本,這裡我們會建立一個空的 web 基本範本:

1
dotnet new web --name MyWeb 

這會在解決方案目錄下產生 MyWeb 專案目錄,接下來我們就要切換到 MyWeb 目錄,開始我們的程式旅途了。

這是 MyWeb 專案目錄初始的目錄架構,包含專案的設定檔 MyWeb.csproj 及基本範例的程式碼,就從這裡開始。首先啟動專案看看這個範例的結果。

1
dotnet run 

啟動後有一些訊息

打開瀏覽器看看結果:

這裡當執行 dotnet run 時,其實 .NET 事先會執行 dotnet build 打包的工作,會產生編譯後的執行檔,可以在專案目錄的 bin 子目錄下發現 MyWeb.exe、MyWeb.dll 等檔案,你可以從檔案總管中直接打開 MyWeb.exe。

首先看一下專案設定檔 MyWeb.csproj。

1
2
3
4
5
6
7
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

</Project>

目前就這樣,紀錄的東西不多,往後如果專案需要安裝 Packages,將會記錄在這裡。 第一堂課所需要 Packages 都已內含不用再另行安裝。

我們就先把未來會用到的 Oracle Package 安裝起來看看。我們可以到 NuGet Gallery 尋找需要的 Packages。

這裡需要 .NET Core 的版本。

將指令複製到你的 VS Code 終端螢幕:

1
dotnet add package Oracle.ManagedDataAccess.Core --version 2.19.60

安裝成功後將會更新 MyWeb.csproj,多了一個 ItemGroup。

1
2
3
<ItemGroup>
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="2.19.60" />
</ItemGroup>

可以觀察一下安裝 Package 的訊息。

  Writing C:\Users\76070049\AppData\Local\Temp\tmpBD4F.tmp
info : 正在將套件 'Oracle.ManagedDataAccess.Core' 的 PackageReference 新增至專案 'I:\C#\TrainSample\MyWeb\MyWeb.csproj'。
info : 正在還原 I:\C#\TrainSample\MyWeb\MyWeb.csproj 的封裝...
info : GET https://api.nuget.org/v3-flatcontainer/oracle.manageddataaccess.core/index.json
info : OK https://api.nuget.org/v3-flatcontainer/oracle.manageddataaccess.core/index.json 710 毫秒
info : GET https://api.nuget.org/v3-flatcontainer/oracle.manageddataaccess.core/2.19.60/oracle.manageddataaccess.core.2.19.60.nupkge.2.19.60.nupkg .2.19.60.nupkg 720 毫秒
info : OK https://api.nuget.org/v3-flatcontainer/oracle.manageddataaccess.core/2.19.60/oracle.manageddataaccess.core.2.19.60.nupkg 720 毫秒
info : 正在安裝 Oracle.ManagedDataAccess.Core 2.19.60。 \MyWeb.csproj'。
info : 套件 'Oracle.ManagedDataAccess.Core' 與專案 'I:\C#\TrainSample\MyWeb\MyWeb.csproj' 中的所有架構相容。
info : 已將套件 'Oracle.ManagedDataAccess.Core' 版本 '2.19.60' 的 PackageReference 新增至檔案 'I:\C#\TrainSample\MyWeb\MyWeb.csproj'。
info : 正在認可還原...
info : 正在將資產檔案寫入磁碟。路徑: I:\C#\TrainSample\MyWeb\obj\project.assets.json
log : I:\C#\TrainSample\MyWeb\MyWeb.csproj 的還原於 3.98 sec 完成。

注意這裡會直接上網下載安裝,如果你無法安裝,有可能是你的電腦被禁止上網,注意最近資訊部的 SSL 政策

有沒有發覺 Packages 的原始碼並沒有安裝在專案目錄中,這與 Node.js 不同,你可以用先前提到的 dotnet nuget locals all –list 查詢 NuGet Packages 的目錄。

開始

當我們下了 dotnet run,.NET 會搜尋有 Main 方法的程式碼,這就是專案的入口點,慣例都會是 Program.cs。修改之前你可以看一下,比較特別的是使用泛型 webBuilder.UseStartup<Startup>( ); 來啟動 Web Server。

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MyWeb
{
public class Program
{
public static void Main(string[] args)
{
MyOutput("(Main) Application start...");

CreateHostBuilder(args).Build().Run();

MyOutput("(Main) Application end...");
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

public static void MyOutput(string message)
{
Console.WriteLine($"[{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}] {message}");
}
}
}
  • 第 30 行加入 MyOutput 靜態方法,這裡沒有使用 Debug 模組,在 VS Code 中開啟 Debug output 比較麻煩,這裡直接使用系統的終端螢幕顯示。
  • 第 27 行使用泛型啟動 Web Server 比較特殊,也因此可以用同一個方法啟動不同類型的 Web Server。也可以直接使用方法鏈接改變預設的的 Web Server 與監聽的 Port。
Program.cs
1
2
3
4
5
6
7
8
9
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseKestrel()
.UseUrls("http://localhost:3000");
});
  • 第 7 行可以不要,預設值本來就是 Kestrel。
  • 第 8 行直接改變監聽的 Port。

這裡直接使用泛型啟動 Startup 類別,現在來看看 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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyWeb
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}

這裡使用的方法與上課的程式碼有些不同,但觀念是一樣的。

  • 第 22 行使用了不同的 IWebHostEnvironment Interface,但用法一樣。
  • 第 31 行用 app.UseEndPoints 定義 HTTP Endpoints,可以直接在此加入另外一個端點。
Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});

endpoints.MapGet("/tainan", async context =>
{
await context.Response.WriteAsync("Hello Tainan! 哈囉,台南!");
});
});

這是最基本的端點設定,稍後再來看 Middlewares。現在則來看看 Application LifeTime 流程。以下是加入 Application Lifttime 的程式碼,試著自己 Coding,不要用複製/貼上,否則你不會發現魔鬼。

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
78
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IHostApplicationLifetime;

namespace MyWeb
{
public class Startup
{
public Startup() {
Program.MyOutput("Startup Contructor called.");
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
Program.MyOutput("Startup.ConfigureServices called.");
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime appLifetime)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

appLifetime.ApplicationStarted.Register(() =>
{
Program.MyOutput("ApplicationLifetime - Started.");
});

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

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

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});

endpoints.MapGet("/tainan", async context =>
{
await context.Response.WriteAsync("Hello Tainan! 哈囉,台南!");
});
});

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

thread.Start();

Program.MyOutput("Startup Configure - Called.");
}
}
}
  • 第 4 行加入 System.Threading 命名空間,因為第 66 行會用到 Thread 類別。
  • 第 11 行加入了一個別名(Alias) IHostApplicationLifetime 因舊的 IApplicationLifetime 已遭廢棄,這裡又同時有兩個 Package 都含有 IHostApplicationLifetime 介面,這裡用一個 Alias 直接指名。IHostApplicationLifetime 會用在第 28 行。
  • 第 28 行加入新的 IWebHostEnvironment 與 Microsoft.Extensions.Hosting.IHostApplicationLifetime 介面,舊的 IHostingEnvironment 與 IApplicationLifetime 已遭廢棄。
  • 第 35 ~ 49 行、66 ~ 73 行沒有不同。

執行結果應該會是:

[2020/04/17 15:19:54] (Main) Application start...
[2020/04/17 15:19:54] Startup Contructor called.
[2020/04/17 15:19:54] Startup.ConfigureServices called.
[2020/04/17 15:19:54] Startup Configure - Called
[2020/04/17 15:19:54] ApplicationLifetime - Started
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:3000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: I:\C#\TrainSample\MyWeb
[2020/04/17 15:19:59] Trigger stop WebHost
[2020/04/17 15:19:59] ApplicationLifetime - Stopping
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
[2020/04/17 15:20:04] ApplicationLifetime - Stopped
[2020/04/17 15:20:04] (Main) Application end...

接著就要來看 Middlewares,在這之前將 66 ~ 73 行程式碼加上註解,否則每次啟動後 5 秒鐘 Web Server 就自動下架了。

Startup.cs
1
2
3
4
5
6
7
8
// var thread = new Thread(new ThreadStart(() =>
// {
// Thread.Sleep(5 * 1000);
// Program.MyOutput("Trigger stop WebHost.");
// appLifetime.StopApplication();
// }));

// thread.Start();

Middlewares

使用 Middlewares 可讓程式碼更有彈性及模組化,可以使用 IApplicationBuilder 介面的 Use 方法註冊。首先我們使用 app.Use( ) 直接在 Startup.cs 中註冊一個 Middleware,但直接在 Startup.cs 中加入 Middleware 程式碼不是好的習慣,會讓程式碼太過龐大很難維護,稍後我們會將 Middlewares 模組化。但我們先來了解 Middlewares 的基本概念。

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
app.Use(async (context, next) =>
{
if (context.Request.Path == "/tainan")
await context.Response.WriteAsync("ASP.Net Core Rocks! Tainan! \n");
else
{
await next();
await context.Response.WriteAsync("Powered by ASP.NET Core \n");
}
});

app.UseRouting();

app.UseEndpoints(endpoints =>
...

在第 50 行的地方插入這幾行程式碼註冊一個 Middleware,這裡插在 app.UseEndpoints 之前。使用 HttpContext.Request 物件的 Path 屬性取得 Url Path,如果是 /tainan 就回應一段字串,否則執行 next( ) 然後再返回一段字串。現在可以實際來看看它的結果。

先看左邊的圖示 Url Path 是 /tainan,它的結果如我們程式碼返回的字串,然後就結束了。後面的 endpoints.MapGet(“/tainan” …) 並沒有執行,因為我們沒有呼叫 next( ) 繼續往下執行。

右邊則是 / 根 Path,因為呼叫 next( ),所以先執行 endpoints.MapGet(“/“ …) 返回 “Hello World!”, 然後返回呼叫它的地方繼續往下執行,返回 “Powered by ASP.NET Core” 字串。

Middlewares 的位置順序很重要,我們將這段 Middleware 程式碼移到 app.UseEndpoints(endpoints => …) 後面,然後重新啟動。

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
...
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});

endpoints.MapGet("/tainan", async context =>
{
await context.Response.WriteAsync("Hello Tainan! 哈囉,台南!");
});
});

app.Use(async (context, next) =>
{
if (context.Request.Path == "/tainan")
await context.Response.WriteAsync("ASP.Net Core Rocks! Tainan! \n");
else
{
await next();
await context.Response.WriteAsync("Powered by ASP.NET Core \n");
}
});
...

現在這個 Middleware 毫無作用。

將它回復到原地方。先前談過將所有 Middlewares 都放在同一個程式碼將很難維護,也很難重複利用,通常都必須模組化程式碼,就來加入一段模組化的 Middleware。

首先在專案目錄下建立子目錄 Middlewares,然後在 Middlewares 目錄按滑鼠右鍵,選擇 “New C# Class”, 你必須安裝 VS Code C# 與 C# Extensions 的 EXTENSIONS。

這兩個 Extensions 是在安裝 .NET 時就會建議的套件。

以下是新的 FirstMiddleware 型態。

FirstMiddleware.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace MyWeb.Middlewares
{
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. \n");
await _next(context);
await context.Response.WriteAsync($"{nameof(FirstMiddleware)} OUT. \n");
}
}
}

程式碼與上課的教材沒甚麼差異。現在可以將它加入 Startup.cs。

FirstMiddleware.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
using MyWeb.Middlewares;
...
app.UseMiddleware<FirstMiddleware>();

app.Use(async (context, next) =>
{
if (context.Request.Path == "/tainan")
await context.Response.WriteAsync("ASP.Net Core Rocks! Tainan! \n");
else
{
await next();
await context.Response.WriteAsync("Powered by ASP.NET Core \n");
}
});
...

第 4 行使用 app.UseMiddleware( ) 泛型加入 FirstMiddleware,注意它的位置,將它放在先前 Middleware 之前。

也可以用 C# 擴充方法將 FirstMiddleware 包裝成靜態方法。新建一個子目錄來擺這些 Extensions,然後在這個目錄下建一個新的類別 CustomMiddlewareExtension。

CustomMiddlewareExtension.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
using Microsoft.AspNetCore.Builder;
using MyWeb.Middlewares;

namespace MyWeb.Extensions
{
public static class CustomMiddlewareExtension
{
public static IApplicationBuilder UseFirstMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<FirstMiddleware>();
}
}
}

現在修改 Startup.cs 改用 CustomMiddlewareExtension。

CustomMiddlewareExtension.cs
1
2
3
4
5
6
7
...
// using MyWeb.Middlewares;
using MyWeb.Extensions;
...
// app.UseMiddleware<FirstMiddleware>();
app.UseFirstMiddleware();
...

這將會有相同的結果,最後的 VS Code 專案目錄結構如下,注意對照一下 namespace 的習慣命名,你也可以將所有的 namespace 都改為與 Program.cs 一樣的 MyWeb,這樣你就不用使用 using import MyWeb.Middlewares namespace,但這似乎不是好的模組命名架構。

祝 有個愉快的 Coding 與學習!