只有累積,沒有奇蹟

2023年10月1日 星期日

[NETCore] ASP.NET Core 中的排程利器 - Coravel

前言
在 ASP.NET 中相信大家都會有過開發 Scheduler 排程的需求,過去可能會使用 ASP.NET 中較有名的框架像是 Quartz.NET 或是 Hangfire,兩種框架各有優缺點小弟不才剛好都有碰過,兩種排程框架各有喜好者可以依據自己的愛好來選用。今天所要介紹的是另一套 Schedule Job 框架 Coravel,作者在設計 API 時用 Fluent interface 方式進行設計,因此在使用上相當直覺與方便,且完整的支援 ASP.NET Core 的應用程式,這篇就來分享一下有關 Corvael 的安裝與基本使用,若有問題或是錯誤的地方歡迎網路的高手大大給予指導

安裝
首先建立一個名為 CoravelLab 的 ASP.NET Core 應用程式專案,接著在 nuget 搜尋並下載 coravel 
下載過程中會提示安裝後的變更,我的習慣會把此設定打開也可以了解安裝時用到哪一些 dll,如果套件沒做甚麼事情又下載一堆 dll 的話我就會...,像是此套件有引用到 system.memory 才發現也支援 cache 功能,而且特別的有用到 system 中的 unsafe 
如果不喜歡 Nuget GUI 進行安裝的話,也可以透過 Nuget Package Console 輸入下列指令
Install-Package Coravel -Version 3.0.0
安裝完畢之後到專案檔底下確認是否有安裝成功
<ItemGroup>
  <PackageReference Include="Coravel" Version="3.0.0" />
</ItemGroup>

使用
基本排程相關設定可以在 Coravel 官網 看到詳細說明,介紹筆記如下

Schedule 執行內容
Coravel 中使用排程服務是以  Schedule  出發,主要是去實作 IScheduler 介面,透過反組譯可以看到 IScheduler 代碼如下
public interface IScheduler
{
    IScheduleInterval Schedule(Action actionToSchedule);

    IScheduleInterval ScheduleAsync(Func<Task> asyncTaskToSchedule);

    IScheduleInterval Schedule<T>() where T : IInvocable;

    IScheduleInterval ScheduleInvocableType(Type invocableType);

    IScheduler OnWorker(string workerName);
}
從上面代碼可以看到 IScheduler 支援多個 API 來設定排程,可以透過 action 方式
scheduler.Schedule(() => Console.WriteLine("This is a book")
或者是指定特定的類別 Class 方式執行排程,使用類別必須實作 IInvocable 的方法
scheduler.Schedule<SomeJobClass>()
另外還支援 ScheduleAsync 方法 
scheduler.ScheduleAsync(async () =>
{
    await Task.Delay(100);
    Console.WriteLine("async task laaa");
})
上面幾種方法必須在使用前先確定有透過 DI 註冊才可以正常使用。

Schedule 執行頻率
Coravel 排程執行時間是透過程式內設定,不透過設定檔來定義,Coravel 內建提供多種方法來提供設定執行的頻率,如果要每秒執行一次就可以透過 EverySecond() 方法執行,或者是指定時間像是每周一的話可以用 Monday() 來設定排程要執行的頻率,舉例來說設定排程在 周末每五分鐘執行一次
scheduler.Schedule<ReportJob>()
    .EveryFiveMinutes()
    .Weekend();
或是 每周一三五的半夜一點跑排程工作
scheduler.Schedule<TodoJob>()
    .DailyAtHour(1)
    .Monday()
    .Wednesday()
    .Friday();
由於方法皆是回傳相同類別這種設計可以一直串流下去,也叫做 Fluent interface 流式接口設計,因此在使用上可以說是相當便利,另外如果內建方法中沒有喜歡的時間也可以使用 cron 方法自行執行時間,舉例來說下面設定是 cron 表達式中每分鐘執行一次 
scheduler.Schedule<TodoJob>()
    .Cron("* * * * *");
除了支援常用的執行時間之外,另外還可透過 cron 方法自行定義自己喜歡的時間可以說是相當方便,在透過 R# 工具看過內建方法的底層也是透過 cron 方式進行設定 (偷學到一招),但有一點要特別注意的是他時間是以 UTC 時間為主,因此在使用上必須特別注意。

在 ASP.NET Core 中使用 
接著透過一個簡單的範例來介紹 Coravel 如何快速上手,開啟專案中 Startup.cs 的 ConfigureServices 加上  AddScheduler  將 Coravel 加入 service
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddScheduler();
}
下一步來使用 Coravel 建立一個簡單的 Schedule,目的是在每一秒透過 console 顯示不用上班的開心並示在畫面上,接著在 Configure 中加入下列代碼
var provider = app.ApplicationServices;
provider.UseScheduler(scheduler =>
{  
    scheduler.Schedule(
        () => Console.WriteLine("周末不用上班,Happy !!")) // 設定 Schedule 執行內容
        .EverySecond()  // 設定 Schedule 執行時間及頻率
});
執行專案,可以看到設定的內容順利的輸出

使用 Schedule<T>
透過 action 來設定相信算蠻簡單的,接著我們再加入一個是透過指定類別的排程,希望在周日每五秒顯示目前的時間,首先先建立 GetDatetimePreFiveSecondJob  類別並實作 IInvocable (需要先 using Coravel.Invocable ),代碼如下
using Coravel.Invocable;

namespace CoravelLab
{
    public class GetDatetimePreFiveSecondJob : IInvocable
    {
        public Task Invoke()
        {
            Console.WriteLine($"Now : {DateTime.Now}");
            return Task.CompletedTask;  
        }
    }
}
接著到 Startup.cs 的 ConfigureServices 將 GetDatetimePreFiveSecondJob 加入 service
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    services.AddScheduler();
    services.AddTransient<GetDatetimePreFiveSecondJob>();
}
到 Configure 定義新增 Schedule 並設定執行的頻率,使用內建設定執行時間 EveryFiveSeconds 以及 Sunday 方法
provider.UseScheduler(scheduler =>
{
    scheduler.Schedule<GetDatetimePreFiveSecondJob>()
        .EveryFiveSeconds()
        .Sunday();
});
接著執行專案看結果,可以看到新增的排程順利執行

感想
希望可以透過這篇文章讓大家了解除了 Quartz.Net 與 Hangfire 之外,也是有輕量級的 Coravel 函式庫的基本使用方式與操作,Coravel 除了支援 Task Schedule 之外另外也提供 Queue、Cache、Event Brocdcasting 以及 Mail 等功能,如果有興趣的朋友也可以到官方網站看看,Hope it helps :)

參考
Coravel

2023年9月18日 星期一

[NETCore] ASP.NET Core 啟動失敗 - 嘗試存取通訊端被拒絕,因為存取權限不足

問題 
接獲同事詢問專案無法正常啟用,專案是使用 ASP.NET Core 2.2 開發並搭配 Kestrel 使用,在過去開發時都正常運作但今天忽然就遭遇啟動異常的狀況,在啟用時會跳出錯誤訊息為 'Unable to bind to http://localhost:5000 on the IPv4 loopback interface: '嘗試存取通訊端被拒絕,因為存取權限不足。''  ,這篇文章就針對此問題的解決方式做分享若是有不清楚或是錯誤的地方歡迎討論予糾正

異常現場
異常發生時畫面如下
Log 中紀錄的詳細錯誤訊息如下
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
      User profile is available. Using 'C:\Users\user\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Unable to bind to http://localhost:5000 on the IPv4 loopback interface: '嘗試存取通訊端被拒絕,因為存取權限不足。'.
warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Unable to bind to http://localhost:5000 on the IPv6 loopback interface: '嘗試存取通訊端被拒絕,因為存取權限不足。'.
crit: Microsoft.AspNetCore.Server.Kestrel[0]
      Unable to start Kestrel.
System.IO.IOException: Failed to bind to address http://localhost:5000. ---> System.AggregateException: One or more errors occurred. (嘗試存取通訊端被拒絕,因為存取權限不足。) (嘗試存取通訊端被拒絕,因為存取權限不足。) ---> System.Net.Sockets.SocketException: 嘗試存取通訊端被拒絕,因為存取權限不足。
   at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, String callerName)
   at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
   at System.Net.Sockets.Socket.Bind(EndPoint localEP)
   at Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.SocketTransport.BindAsync()
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.<>c__DisplayClass21_0`1.<<StartAsync>g__OnBind|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindEndpointAsync(ListenOptions endpoint, AddressBindContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context)
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Server.Kestrel.Core.LocalhostListenOptions.BindAsync(AddressBindContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.AddressesStrategy.BindAsync(AddressBindContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.AddressBinder.BindAsync(IServerAddressesFeature addresses, KestrelServerOptions serverOptions, ILogger logger, Func`2 createBinding)
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)

C:\Program Files\dotnet\dotnet.exe (process 31468) exited with code -1.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . . 
根據錯誤訊息提供的線索,異常原因可能是因為 Kestrel 在啟動時失敗造成 (Startup failure),Kestrel 預設是使用 5000 port,懷疑該已被占用造成啟用時使用該 port 號卻無法正常執行,才跳出 嘗試存取通訊端被拒絕,因為存取權限不足 錯誤訊息,首先先來釐清推測是否正確,查詢 port 使用狀況可以透過以下兩種方式

命令提示字元
開啟 Windows Terminal 使用命令提示字元,輸入  netstat -nat | findstr 5000  指令查看 port 使用狀況 
PS C:\> netstat -nat | findstr 5000 
查詢後可以看到目前系統中 5000 port 已被占用

TCPView
TCPView 是一個非常實用的小工具,可以顯示目前系統中 TCP 與 UDP 連接的狀態,不用在命令提示字元下指令就可以看到需要的資訊,之前用過此工具幫忙在 Production 解決不少問題,如果之前未使用過強烈建議可以下載使用看看,在微軟官方就有提供下載連結 : 傳送門,解完壓縮之後直接執行 TCPView.exe 執行檔,可以看到 5000 Port 已被其他應用程式所占用

解決方案
透過上述步驟可以看到,主要原因是因為 Kestrel 所要用的 Port 已被其他應用程式所佔用,占用的應用程式 ProcessID 是 4,可以透過以下幾種方式解決

設定 IIS
在過去的經驗中告訴我們 system ProcessID 為 4 時候 87% 是 IIS,推測可能是因為在 IIS 中某一個 Application 中有設定使用到該機器的 5000 Port,可以開啟 IIS 檢查在建立 Application 所設定到的 Port 號,有與 5000 衝突的部分替換為新的 Port 號即可,果然在同事的電腦中發現測是的應用程式已將指定 Port 占用,調整後 ASP.NET Core 專案即可正常運行。

設定 Kestrel 
另外一種方式是調整 Kestrel 的指定 Port,在 ASP.NET Core 預設是使用 5000 port,如果電腦中應用程式的 5000 Port 是無法調整的,及可以透過設定的方式將預設的 Port 改用其他指定 port 避免衝突的狀況發生,如果是使用 ASP.NET Core Web template 建立專案,其中  CreateDefaultBuilder  方法會呼叫 serverOptions.Configure(context.Configuration.GetSection("Kestrel")) 載入設定,因此我們可以到 appsettings.json 調整,以下列設定為例是將 Kestrel port 指定為 5050
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5050"
      }
    }
  }, 
  ....other settings
} 
以上兩種方式都可以解決當 Kestrel 預設 Port 被占用,造成應用程式啟動失敗的問題,各位看官可以依據情境選擇適合是自己的解決方案,若是有更好的方法也歡迎提出來一起討論,Thanks & Happy Coding :)

參考
ASP.NET Core 中的 Kestrel 網頁伺服器實作

2023年9月3日 星期日

[NETCore] ASP.NET Core 建立與解析 QueryString 參數

說明
之前介紹過在 .NET 中可以使用 Utility.ParseQueryString 處理 Url 中的參數,傳送門 : 使用 ParseQueryString 取得網址參數,但所使用的 System.Web 命名空間僅存在於 ASP.NET Framework 不支援 ASP.NET Core,在搜尋更好的解決方案中發現了在 ASP.NET Core 提供新的 API - QueryHelpers 可以達到同樣效果,此篇介紹如何使用 queryHelpers 建立與解析 querystring 參數,若有問題歡迎留言討論

下載
QueryHelpers 從 .NET Core 1.0 RC2 就開始支援,命名空間為 Microsoft.AspNetCore.WebUtilities,使用前須先透過 Nuget Package Manager 下載
Install-Package Microsoft.AspNetCore.WebUtilities -Version 2.2.0
Visual Studio 2019 專案點擊 Project 檔案可直接開啟,確認是否有安裝成功

產生 QueryString
QueryHelpers 使用  AddQueryString  產生 querystring 內容,使用方式相當簡單直接看 Sample Code 
string url = QueryHelpers.AddQueryString("/api/demo", "key1", "value1");
// result : api/demo?key1=value1
有多個參數需求時可以傳入 Dictionary
var queryParams = new Dictionary<string, string>()
{
    {"key1", "value1" },
    {"key2", "value2" },
    {"key3", "value3" }
};
url = QueryHelpers.AddQueryString("/api/demo/list", queryParams); 
// result : api/demo/list?key1=value1&key2=value2&key3=value3
輸入中文字,會自動 url Encode (覺得實用,好棒棒!!!)
url = QueryHelpers.AddQueryString("/api/demo/list", "高雄", "發大財");
// result : api/demo/list?%E9%AB%98%E9%9B%84=%E7%99%BC%E5%A4%A7%E8%B2%A1

解析 QueryString
QueryHelpers 使用  ParseQuery  解析 Url 的 querystring 內容,以下範例為透過 ParseQuery 解析 QueryString 內容,並使用 selectMany 反序列化為 KeyValuePairs 方便後續操作
ar rawurl = "https://marcusblog.com/api/product/list?key1=value1&key2=value2&key3=value3&key1=value1&key5=";

var uri = new Uri(rawurl);
var baseUri = uri.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped);

var query = QueryHelpers.ParseQuery(uri.Query);

var items = query.SelectMany(x => x.Value, (col, value) => new KeyValuePair<string, string>(col.Key, value)).ToList();

// result : 
//      key1/value1
//      key2/value2
//      key3/value3

同場加映 - QueryBuilder
在 ASP.NET Core 中產生 QueryString 也可以透過 Microsoft.AspNetCore.Http.Extensions 中的  Querybuilder  方法,透過 Add 方法將參數帶入並使用  ToQueryString 輸出
var qs = new QueryBuilder();
qs.Add("key1", "test");
qs.Add("key2", "1111");
var result = qs.ToQueryString();

// result : ?key1=test&key2=1111
上面介紹兩種在 ASP.NET Core API 產生 QueryString 的方式,都可以達到目的,各位在使用上可以依據喜好來決定使用哪一種 :)

參考

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

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com