只有累積,沒有奇蹟

2021年11月17日 星期三

[NETCore] 如何在 ASP.NET Core Middleware 加上單元測試 Unititest

前言
Middleware 在 ASP.NET Core 開發時是個很常見的功能,概念很像 ASP.NET Application Life cycle 管線的 Handler 機制 (若對於 Life Cycle 想了解更多可以看之前寫的文章 Application Life Cycle),提供開發者可以在 Request 進入到 Application 前加上客製化的邏輯,實務上用起來挺方便的也蠻好用的,在加上 middleware 相關邏輯後也會加上單元測試,確保所撰寫的邏輯有驗證過且是沒有問題,也避免在 middleware 異常時 Devops 同仁半夜叫你起床處理(萬萬不可阿,這篇文章要說明的是如何在 middleware 加上 unittest,對有經驗的開發者相信是一塊小蛋糕,內容若有問題歡迎提出來一起討論。


建立檢查 Token Middleware
舉一個常用的例子,實務上在與第三方在做串接時需要 token 作為對方請求的驗證,作為過濾不必要的請求以節省伺服器的資源。全部 API 接口都需要做 token 檢查因此將此邏輯檢查可以放在 Middleware,首先建立一個新的 .NET Core 3.1 WebAPI 專案,接著建立 TokenVerificationMiddleware.cs 類別,在此類別中加入下列代碼
public class TokenVerificationMiddleware
{
	private readonly RequestDelegate _next;

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

	public async Task Invoke(HttpContext context)
	{
		if (context.Request.Headers.ContainsKey("token") && context.Request.Headers["token"] == "marcusblog")
		{
			await _next.Invoke(context);
		}
		else
		{
			context.Response.StatusCode = 403;  // UnAuthorized
			await context.Response.WriteAsync("Invalid request token");
			return;
		}
	}
}
在此類別的代碼中定義了當請求來時檢查 Request 的 Header 資訊是否有包含 token 關鍵字,token 有的話是否等於 marcusblog,如果都符合就讓此請求往下一步進行進入到下一個流程像是 Action 邏輯;相反的如果不符合 token 檢查邏輯,就會回傳 403 等 Status Code。
當建立好檢查 Token 的 Middleware 之後,接著開啟專案中的 StartUp.cs 類別在 Configure 方法中加上以下代碼
app.UseMiddleware();
上述是使用內建的 UseMiddleware 方法並定義指定的 Middleware 類別,另外還可以使用擴充方法(Extensions Method)達到使用 Middleware,也是個人比較推薦的方式,將要使用的 Middleware 統一集中在特定的class中代碼在閱讀上較為容易理解,使用方式是新增 ApplicationBuilderExtension 類別,裡面新增 UseTokenVerificationMiddleware 方法並定義回傳 IApplicationBuilder 型別,內容與在 startup.cs 的一樣
public static class ApplicationBuilderExtension
{
	public static IApplicationBuilder UseTokenVerificationMiddleware(this IApplicationBuilder builder)
	{
		return builder.UseMiddleware();
	}
}
接著再回到 Configure 方法將 UseTokenVerificationMiddleware 取代原本的方法
//app.UseMiddleware();
app.UseTokenVerificationMiddleware();
開啟偵錯進行測試,可以發現網頁打開會因為 Request 中沒有在 Header 中帶 token 參數出現 Invalid request token,畫面如下


加上單元測試
一般來說請求進到 Application 的 Request 請求的內容會存在於 HttpContext 中,接著再把 HttpContext 的請求傳遞到所指定的 Middleware 邏輯中,也就是在建構子中的參數 HttpContext,那麼要進行單元測試的話第一步是要先模擬請求的 HttpContext 物件,這邊可以使用 DefaultHttpContext 達到這目的,DefaultHttpContext 繼承了 HttpContext 抽象類別,讓我們在單元測試中更為方便,舉例來說如果要模擬 hpptContext 物件並在 Header 加上 token 內容用下列方式就可以達到
var context = new DefaultHttpContext();
context.Request.Headers.Add("token", "this is a book");
DefaultHttpContext 還可以指定測試的路徑(Path)、內容(Body)、ContentType 或是可以指定 Cookie Form 及 QueryString 等常見的設定值,使用上相當的簡易與方便,放在測試案例中代碼如下
public class TokenVerificationMiddlewareTest
{
    private TokenVerificationMiddleware _target;
    [SetUp]
    public void Initial()
    {
        _target = new TokenVerificationMiddleware(null);
    }

    [Test]
    public void Correct_Header_Should_Return_Success()
    {
        var context = new DefaultHttpContext();
        context.Request.Headers.Add("token", "marcusblog");

        var result = _target.Invoke(context);
        context.Response.StatusCode.Should().Be(200);
    }

    [Test]
    public void Without_Header_Should_Return_UnAuthorized()
    {
        var context = new DefaultHttpContext();

        var result = _target.Invoke(context);
        context.Response.StatusCode.Should().Be(403);
    }

    [Test]
    public void Empty_Header_Should_Return_UnAuthorized()
    {
        var context = new DefaultHttpContext();

        var result = _target.Invoke(context);
        context.Response.StatusCode.Should().Be(403);
    }

    [Test]
    public void Wrong_Header_Should_Return_UnAuthorized()
    {
        var context = new DefaultHttpContext();
        context.Request.Headers.Add("token", "wrongtoken");

        var result = _target.Invoke(context);
        context.Response.StatusCode.Should().Be(403);
    }
}
以上是使用 nUnit 搭配 FluentAssertions 套件,測試幾種常見的情境像是 token 為空、header 為空、帶錯誤的 token及帶正確的 token 值 測試後的結果 Pass 成功 !
大功告成,打完收工

感想
在 ASP.NET Core 有了 httpDefaultContext 之後,在針對 Middleware 撰寫單元測試變得方便許多,對有經驗的開發者來說也是小蛋糕一塊,如果有不清楚的地方歡迎一起討論,hope it helps !

參考
為 .NET Core Middleware 加上 Unit Test
DefaultHttpContext Class
ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界
ASP.NET Core 中介軟體
ASP.NET Core 2.1 middlewares part 2: Unit test a custom middleware




2021年11月3日 星期三

[NETCore] ASP.NET Core 使用強型別取代 IOption 注入配置

前言
之前的 如何取得 appsettings.json 組態設定 文章中有介紹在 ASP.NET Core 中透過 IOptions 方法取得設定檔的方法,在需要用到的地方注入 IOptions 取得設定類別的資訊,相信使用上並不困難在 MSDN 官方推薦作法也是如此,但如果開發一陣子之後可以發現到處都是 IOptios 類別,這篇文章介紹如何使用擴充 IServiceCollection 的方法來降低對 IOptios 的依賴,若有問題或是錯誤的地方歡迎各方高手大大一起討論或給予指導

IOption
之首先先來簡單回顧  IOptions<T>  的傳統用法,透過微軟 MSDN 中 IOptions 介紹得知要使用前需先引用 Microsoft.Extensions.Options,為了方便快速了解差異性,這裡建立一個 ASP.NET Core Web 應用程式範例來說明,在新增完應用程式後在 appsetting.json 加入自己定義的  mySettings  設定資訊提供 Name 以及 Title 屬性
"MySettings": {
    "Name": "Marcus",
    "Title": "9527"
  } 
接著要取得設定檔的內容,這邊建立與 Config 內容欄位相同的強型別的 class 物件
public class MySettings
{
    public string Name { get; set; }
    public string Title { get; set; }
} 
在 ConfigureServices 中加入下列代碼,用意是將 appsettinss.json 中的資訊加載到 MySettings 中
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddOptions();
    services.Configure<MySettings>(Configuration.GetSection("MySettings"));
} 
接著就可以在 Value Controll 使用 IOptions<T> 取得設定檔的資訊,代碼如下
public class ValuesController : ControllerBase
{
    public MySettings _mySettings { get; set; }

    public ValuesController(IOptions<MySettings> settings)
    {
        _mySettings = settings.Value;
    }

    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { _mySettings.Name, _mySettings.Title };
    } 

不使用 IOptions 注入
以上快速地回顧 IOptions<T> 的用法,就可以簡單使用 IOption<T> 取得 appsettings.json 設定資訊的方法,但是這意味著你的代碼與 IOptions 有著強制依賴的關係,你有多少 Controller 就需要在各別的 Controller 中都 using 所需要的 Microsoft.Extensions.Options,除非 appsettings.json 中的設定很常異動需要進行重新載入( 這時可以使用  IOptionMonitor  而不是 IOptions ),否則大部分的使用情境中 config 設定都是較少異動的,參考此文章之後有了新的解法,我們可以新增一個類別並針對 IServiceCollection 加入擴充方法,代碼如下
public static class ServiceCollectionExtensions
{
    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration) where TConfig : class, new()
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));

        var config = new TConfig();
        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    }
} 
這裡我們在 startup.cs 時手動使用  Microsoft.Extensions.Configuration.Binder  來綁定設定檔,並指定服務容器生命週期 (LifeTime) 為 Singleton,在 ASP.NET Core DI 預設提供的 Lifetime 有下列三種
  • Transient : 每次請求時都會產生新的 Instance
  • Scoped : 每個 http Request 都會產生一份 Instance
  • Singleton : 整個 Application 只會有一份 Instance
可以依據所需要情境作調整,如果想了解差異可以參考之前的文章 : 傳送門 

新增完擴充方法之後,接著下一步就是在 ConfigureServices 改用新增的擴充方法 confugurePOCO
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddOptions();
    services.ConfigurePOCO<MySettings>(Configuration.GetSection("MySettings"));
    //services.Configure<MySettings>(Configuration.GetSection("MySettings"));
} 
接著在回到要使用的地方也就是範例的 ValueController, 將 IOptions 依賴移除改為強行別的 MySettings
public MySettings _mySettings { get; set; }

public ValuesController(MySettings settings)
{
    _mySettings = settings;
} 
在重新執行應用程式,可以發現應用程式執行正常無誤
如果你對於擴充方法很熟悉,也可以針對自己的需求來自訂所需的方法簽章,例如新增一個 TConfig 參數做為繫結設定檔代碼如下
public static class ServiceCollectionExtensions
{
    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration, TConfig config) where TConfig : class
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (config == null) throw new ArgumentNullException(nameof(config));
 
        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    }
} 
使用方式先 new 之後作為參數傳入
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
 
    var mySettings = new MySettings("foo"); 
    services.ConfigurePOCO(Configuration.GetSection("MySettings"), mySettings);
} 
或者是新增 Func<TConfig> 參數透過委派方法新增 TConfig 的 instance
public static class ServiceCollectionExtensions
{
    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration, Func<TConfig> pocoProvider) where TConfig : class
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (pocoProvider == null) throw new ArgumentNullException(nameof(pocoProvider));

        var config = pocoProvider();
        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    } 
}
使用方式如下
public void ConfigureServices(IServiceCollection services)
{
    //...
    services.ConfigurePOCO(Configuration.GetSection("MySettings"), () => new MySettings("foo"));
    //...
} 
如果想了解更多細節,可以參考 Strongly typed configuration in ASP.NET Core without IOptions<T> 取得更多資訊,希望透過以上的介紹,可以讓跟我有一樣困擾的開發者得到新的作法,Hope it helps :)

2021年10月23日 星期六

[conference] MOPCON 2021 - 探討開源平台 SigNoz,實施大規模分佈系統遙測的挑戰

分享心得
網站上線後往往才是挑戰的開始,在大神 Ant 推坑下報名了 MOPCON,跟與會的朋友分享 #Signoz 這套開源的工具,其中也帶到 #可觀察性 與 #監控平台應具備特性 等自己有興趣的議題。也趁空檔跟一些講者聊聊各自研究的議題,在活動結束後回想,都覺得收穫最多的不是別人而是自己,也很感謝在準備過程中協助 review 的朋友跟夥伴,希望這次在濁水溪以南最強大行動科技年會的分享聽眾都有收穫,等待 MOPCON 官方把影片上架構在分享給大家,若有任何問題歡迎提出來一起討論。

議程介紹
主題 : 探討開源平台 SigNoz,實施大規模分佈系統遙測的挑戰
在服務切割越來越微細的世代,系統上線後的監控也變得越來越複雜
為了監控雲端應用服務上線後的可用性,可能會使用不同的工具像是 Prometheus、Grafana 或是 Jaeger 來達到其目的
,或是使用一些整合性高須付費的 SaaS 服務像是 DataDog、NewRelic 等。
有沒有一個框架可以在監控上整合各服務間的問題且免費的呢?
SigNoz 是一個開源且功能強大的監控平台,透過此框架內建常見的 RPS, 50th/90th/99th 等監控指標,當請求進來後在各個服務的跟蹤指標資訊都可以查看到,發生問題時可以快速找到根本問題與排除。
透過 OpenTelemetry 函式庫建立與管理各服務的監控數據,支援跨程式語言能夠整合常見的服務與工具。
在本議程會與大家分享下面幾個議題
➊ SigNoz 要解決什麼問題呢 ?
➋ OpenTelemetry 是甚麼 ? OpenTelemetry 是如何做到服務之間的數據追蹤、蒐集與分析的呢
➌ SigNoz 的優點與缺點是什麼呢 ?

主辦單位 : MOPCON 堅持濁水溪以南,最大行動科技年會
議程表 : 連結
共筆 : https://hackmd.io/@mopcon/2021/%2FDRb25K2eS5KD96WcAgHBzQ



影片連結

2021年9月29日 星期三

[NETCore] ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Part 3

前言
這是 ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net 系列文第三篇,這系列文的目標是使用 ASP.NET Core Generic Host 搭配排程套件 Quartz.Net,程式註冊為 Windows Service 服務執行,
第一篇介紹了 Gereric Host 泛型主機的基本設定,第二篇介紹 ASP.NET Core 與 Quartz.Net 的整合,接下來這一篇是要將開發好的應用程式註冊為 Windows Service,若有問題或是錯誤的地方歡迎各位高手給予指導
GenericHostLab SampleCode 傳送門 : http://bit.ly/2Y1mqYN
ServiceBase
我們的目標是將開發完的應用程式註冊使用 Windows Service 來執行,因此在 Service 執行的動作像是啟動、停止、關閉等動作都會希望知道並透過 log 記錄下來,才可以更清楚的瞭解其運行方式與錯誤時偵錯,在 ASP.NET 中可以透過 system.serviceprocess 來取得 Service 資訊,透過指令到 nuget 下載此套件
Install-Package System.ServiceProcess.ServiceController -Version 4.5.0
接著新增一個類別 ServiceBaseLifetime 繼承 system.ServiceProcess 命名空間中的 ServiceBase 類別與實作 IHostLifetime 介面,在覆寫 serviceBase 方法像是 OnStart、OnStop 以及 run 等,並在希望關注的方法中加上 log 方便蒐集更多 Service 執行時的 log 資訊,代碼如下
namespace GenericHostLab.Service
{
    public class ServiceBaseLifetime : ServiceBase, IHostLifetime
    {
        private readonly TaskCompletionSource<object> delayStart = new TaskCompletionSource<object>();

        public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
        {
            ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
        }

        private IApplicationLifetime ApplicationLifetime { get; }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => delayStart.TrySetCanceled());
            ApplicationLifetime.ApplicationStopping.Register(Stop);
            ApplicationLifetime.ApplicationStopped.Register(Stop);
            new Thread(Run).Start();
            return delayStart.Task;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Stop();
            return Task.CompletedTask;
        }

        private void Run()
        {
            try
            {
                Run(this);
                delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
            }
            catch (Exception ex)
            {
                delayStart.TrySetException(ex);
            }
        }

        protected override void OnStart(string[] args)
        {
            delayStart.TrySetResult(null);
            base.OnStart(args);
        }
        protected override void OnStop()
        {
            ApplicationLifetime.StopApplication();
            base.OnStop();
        }

        protected override void OnShutdown()
        {
            base.OnShutdown();
        }
    }
}
新增 ServiceBaseLifetime 類別後,下一步可以針對 IHostBuilder 加入擴充方法,方便在 Main 中直接使用,在 Service 中新增 ServiceBaseLifetimeHostExtensions 類別,代碼如下
namespace GenericHostLab.Service
{
    public static class ServiceBaseLifetimeHostExtensions
    {
        private static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
        }
    }
} 
其中 UseServiceBaseLifetime 將剛剛新增的 ServiceBaseLifetime 服務註冊到 DI 容器中,公開方法為 RunAsServiceAsync 提供外面使用,因此可以在 main 方法中直接使用  RunAsServiceAsync ,代碼如下
static async Task Main(string[] args)
{
    var isService = !(Debugger.IsAttached || args.ToList().Contains("--console"));

    try
    {
        var builder = CreateHostBuilder();

        if (isService)
        {
            await builder.RunAsServiceAsync();
        }
        else
        {
            await builder.RunConsoleAsync();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}
代碼中第三行是取得是否使用 debug 來執行,第七行為執行一開始介紹的 CreateHostBuilder 產生 HostBuilder,接著在根據 isService 是否為 windows service 來決定執行的方式,如果是 debug 的話會 RunConsoleAsync ,如果是透過 Service 執行則是跑自定義的擴充方法 RunAsServiceAsync。

Publish
在這一步中將透過 Visual Studio 內建的部署功能將應用程式部署到指定資料夾,在專案檔點擊右鍵按下 publish 功能,會請你選擇要發布的位置的方式是 Azure 或是 資料夾,以這次的範例中要建立 Windows Service,因此選擇指定資料夾 D:\Web\GenericHostedService
在部署的過程中也可以設定發布相關的 Release 資訊,像是 Debug 或是 Release Mode、發佈時的 ASP.NET Core 版本、Runtime 環境以及要發布到的路徑等設定,由於應用程式排程要擺放的主機 OS 環境為 Windows,因此 runtime 選擇 win64 
確認設定無誤之後,就可以按下 publish 將應用程式發布到指定的資料夾中,待發布完成之後我們可以來到指定的資料夾確認是否有發佈成功,由於剛剛所選擇的 runtime 環境是 Windows 因此可以在資料夾底下看到 ProjectName.exe 執行檔,如果環境選擇的是 Linux 則不會有 exe 只會有 .dll,這邊可以點擊 .exe 看看 Console 執行檔是否正常執行
如果發現部署的結果沒有執行檔,可以開啟專案檢查看看是否有遺漏對應的 property,在 Visual Studio 2019 快速開啟的方式是直接點專案就可以開啟 .csproj,舊的 Visual Studio 版本則是透過找到檔案後再開啟方式確認
<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.2</TargetFramework>
  <LangVersion>7.1</LangVersion>
</PropertyGroup>

Register Windows Service
終於進入最後一個步驟,如果有從頭到尾看完的朋友辛苦了(跪拜) !! 這一步要將寫好的應用程式註冊到 Windows Service 服務,Windows Service 註冊方式在之前 註冊 Windows Service 服務 文章中有介紹過基本指令還有可以透過命令提示字元以及 powershell 兩種方式註冊,有興趣可以到文章了解這裡就不在多加描述,我們直接使用 powershell 指令來進行其註冊動作,輸入下列指令將應用程式註冊至服務中
New-Service -Name "GenericHostService" -BinaryPathName "D:\Web\GenericHostedService\GenericHostLab.exe" -DisplayName "GenericHostLab Service" -StartupType Manual -Description "This is a test service."
參數介紹


  • Name : 指定服務的名稱
  • BinaryPathName : 服務的執行檔案路徑位置
  • DisplayName : 顯示的名稱
  • StartupType : 啟動類型
  • Description : 描述
  • 建立成功之後,可以再執行指令進行啟動服務
    start-service -Name "GenericHostService""
    啟動完成就可以在 Service 裡看到剛剛開啟的服務
    另外在 2019 年 6 月 Microsoft 推出 Windows Terminal,可搭配自己喜歡的桌面在輸入指令時使用(爽度破表!!!!),這邊直接用 Windows Terminal Preview 進行輸入 powershell 指令,使用 admin 開啟 Windows Terminal Preview 軟體之後,上述指令輸入之後完整過程如下
    確認服務正常執行,正式宣告完成本次任務打完收工 !!!! 

    感想
    終於完成在 .NET Core 建立排程服務的系列文,這系列文是自己覺得近期工作上比較值得分享的項目之一,從一開始對於 .NET Core Host Service 陌生開始摸索到程式可以正常啟動,到後來深入了解與同事討論針對重要細節進行研究在重構程式碼,在完成專案後自己感覺也對 ASP.NET Core 也更進一步的認識,因此在整理成文章的過程也花了不少的時間對細節來做解釋,目的就是希望有遇到跟我相同需求的朋友可以更快地透過這系列文來上手,省下開發者踩雷到處碰壁失敗的經驗,也很感謝耐心看完的開發者朋友們,如果有更好的做法與建議歡迎提出來一起討論,最後如果有想了解更多 ASP.NET Core 的朋友推薦可以閱讀 Andrew Lock 部落格文章,相信對開發更有幫助,Happy Coding :)

    Copyright © m@rcus 學習筆記 | Powered by Blogger

    Design by Anders Noren | Blogger Theme by NewBloggerThemes.com