2022年5月6日 星期五

c# async await非同步

在c#有async await非同步的東西非常好用,不過有些地方需要注意,我這邊舉四個案例

案例一:

寫了一個非同步的method,但呼叫的時候沒有加await

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                TestAsync();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }


        private static async Task TestAsync()
        {
            await Task.Delay(2000);
            Console.WriteLine("Test...");
            throw new Exception("Test Exception");
        }
    }
}


案例一的結果TestAsync等於射後不理,而且method裡面噴錯也不會被catch住喔!!


案例二:

寫了一個void method但有掛async

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                TestAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }


        private static async void TestAsync()
        {
            await Task.Delay(2000);
            Console.WriteLine("Test...");
            throw new Exception("Test Exception");
        }
    }
}












案例二的結果會先執行Hello World,所以並不會等待TestAsync的回應,不過是可以catch到exception


案例三:

寫了一個非同步的method,但呼叫的時候有加await

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                await TestAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }


        private static async Task TestAsync()
        {
            await Task.Delay(2000);
            Console.WriteLine("Test...");
            throw new Exception("Test Exception");
        }
    }
}



案例三的結果會等待TestAsync的回應,也可以catch到exception


案例四:

寫了一個非同步的method,先指定task1 task2 最後兩個一起await

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                var task1 = TestAsync();
                var task2 = TestAsync();

                await task1;
                await task2;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.WriteLine("Hello World!");
            Console.ReadLine();
        }


        private static async Task TestAsync()
        {
            await Task.Delay(2000);
            Console.WriteLine("Test...");
        }
    }
}


案例四的結果會等待task1 task2的回應,也就是說task1與task2會並行執行,也可以catch到exception

2022年2月18日 星期五

asp.net core nlog trace id

最近公司幾次技術分享都有提到traceId,當你程式在執行A > B > C

如果沒有traceId你會非常難知道上下文的關係,你會不知道B被誰呼叫,C又被誰呼叫

整段的請求流程會不清楚,出錯了也很難追蹤哪個環節出錯了

在分散式的系統裡更為重要,例如在A站台呼叫了B站台的API,B站台又呼叫了C站台的API

如果有了traceId,你會知道整個請求流程,在除錯上也會很快的定位方向!!

那asp.net core該如何實現,其實可以使用NLog來記錄traceId,再搭配Middleware來串接分散式的請求

首先我們會先安裝NLog,NLog的config如下

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
	  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	  autoReload="true">

	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>

	<targets>
		<target xsi:type="File" name="File" fileName="${basedir}/logs/${shortdate}.log"
	            layout="TimeStamp=${longdate} Level=${uppercase:${level}} TraceId=${aspnet-item:variable=TraceId} Message=${message}" />
	</targets>

	<rules>
		<logger name="*" minlevel="Debug" writeTo="File" />
	</rules>
</nlog>

接下來再建一個LoggingMiddleware

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication6
{
    public class LoggingMiddleware
    {
        private readonly ILogger<LoggingMiddleware> logger;
        private readonly RequestDelegate next;

        public LoggingMiddleware(ILogger<LoggingMiddleware> logger, RequestDelegate next)
        {
            this.logger = logger;
            this.next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            var parentTraceId = context.Request.Headers["X-Trace-Id"];
            if (!string.IsNullOrEmpty(parentTraceId))
                context.Items["TraceId"] = parentTraceId;
            else
                context.Items["TraceId"] = Guid.NewGuid().ToString();
            await next(context);
        }
    }
}

這個Middleware的用意是如果別的站台呼叫我們API的時候而且Header有帶X-Trace-Id

我們就會把traceId帶入context.Items["TraceId"],給NLog使用

如果沒有就自己產一個新的Guid,帶入NLog使用,在Startup.cs加入Middleware

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

            app.UseHttpsRedirection();
            app.UseMiddleware<LoggingMiddleware>();
            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

這個時候我們用一個LogController來測試一下

using CorrelationId.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace WebApplication6.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class LogController : ControllerBase
    {
        private readonly ILogger<LogController> _logger;

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

        [HttpGet]
        [Route("Test1")]
        public IEnumerable<WeatherForecast> Test1()
        {
            _logger.LogInformation("Test1");
            AAA();
            BBB();
            return new List<WeatherForecast>();
        }


        private void AAA()
        {
            _logger.LogInformation("AAA");
        }

        private void BBB()
        {
            _logger.LogInformation("BBB");
        }
    }
}
所以呼叫Test1會寫入這樣的Log
TimeStamp=2022-02-19 15:08:05.5464 Level=INFO TraceId=89214b64-d547-4f49-84f1-8da1e0ff887a Message=Test1
TimeStamp=2022-02-19 15:08:05.5517 Level=INFO TraceId=89214b64-d547-4f49-84f1-8da1e0ff887a Message=AAA
TimeStamp=2022-02-19 15:08:05.5517 Level=INFO TraceId=89214b64-d547-4f49-84f1-8da1e0ff887a Message=BBB
  
如果外部有帶X-trace-Id 1234進來的話,Log會是這樣呈現
TimeStamp=2022-02-19 15:08:28.7794 Level=INFO TraceId=1234 Message=Test1
TimeStamp=2022-02-19 15:08:28.7832 Level=INFO TraceId=1234 Message=AAA
TimeStamp=2022-02-19 15:08:28.7832 Level=INFO TraceId=1234 Message=BBB
  
所以當在Log收集的時候,就會很清楚整條請求的流程

2022年1月22日 星期六

asp.net core web使用appleId登入

最近在套第三方Web AppleId登入遇到了一點眉角,在這邊紀錄一下

Apple developer設定

在 Certificates, Identifiers & Profiles -> Identifiers -> Service IDs

按旁邊 + 號,選擇 Service ID 來創建您的 Service ID這裡有二個選項要填:








Description這裏填入您網站的名字(注意:這個值會顯示在前台網站給使用者看到)

Identifier你可以隨意取一個,作為辨識用勾選 Sign in with Apple 並按旁邊的 Configure










Primary App ID選擇你主要 App 的 App ID Register Website URLs

Domains and Subdomains 這裏填入您網站的網域
(注意:你的網站必須要有 https,不可用 localhost 或者 IP ,否則這裡不會過)

Return URLs 這裡填入回跳用的 Redirect URI
(一樣的規則,你的網站必須要有 https,不可用 localhost 或者 IP ,否則這裡不會過)

















Sign Key 驗證金鑰

我們需要建立一個 Sign Key 在等一下跟蘋果 API 做驗證使用,這部分因為網站跟 App 驗證流程後半段是一樣的,不管支援哪一個部分都要做。

在 Certificates, Identifiers & Profiles -> Keys 按旁邊 + 號,建立一個 Key






Key name可以自行取名,勾選 Sign in with Apple 並按旁邊的 Configure

Primary App ID選擇主要 App 使用的 App ID

Grouped App IDs取名會把網站跟 App 群組綁在一起,按下 Continue 之後會讓你下載一個私鑰 p8 檔案,注意這只能被下載一次,請好好保存。如果不見的話就只能再重新產生一個。


以上設定完之後,開始實作登入,Apple有一份Web登入的文件

https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms


Apple登入的網址

https://appleid.apple.com/auth/authorize?client_id={0}&redirect_uri={1}&response_type=code&scope=openid%20email%20name&response_mode=form_post&state={2}


client_id

它可以是 App ID (也就是 Bundle ID) 也可以是 Service ID。

如果要 網站端做登入,它就會是 Service ID。

redirect_uri

OAuth 之後要轉跳的網址

Certificates, Identifiers & Profiles -> Identifiers -> Service IDs -> 您的 Service ID -> Configure -> Return URLs

state 

一個您設定的,辨識用的字串

scope

我都填固定值 name email (注意:中間有空格要加入%20)

以上參數組好之後,可以直接連結會出現Apple登入畫面,登入完成後他會Post到你指定的redirect_uri,這邊注意一下他是Post不是Get喔,Facebook跟Line都是使用Get的!!


Apple CallBack處理

如文件所示他會callback的參數有code、id_token、state、user、error

scope

我都填固定值 name email (注意:中間有空格要加入%20)

code

有效期為五分鐘的一次性授權碼

user

首次登入才會有值,裡面會有email、firstname、lastname所以要自己保存好,因為下次登入就不會有值了!
(若要測試可以把登入的App從AppleId移除,就可以再出現首次)

id_token

基本上是空值

error

返回的錯誤碼


Call Token API

使用code呼叫另一個Token API取得相關資料

Token API文件

https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

如文件所示需帶入的參數為client_id、client_secret、code、grant_type、refresh_token、redirect_uri

client_id

如果要 網站端做登入,它就會是 Service ID

code

就是CallBack回來的code參數

grant_type

這裡我們填 authorization_code 來交換 AccessToken

refresh_token

這個不用給

redirect_uri

OAuth 之後要轉跳的網址

client_secret

一個 JWT 格式的要求文件,並利用前面申請的 Sign Key 來簽章


這邊最難的就是取client_secret,取這個需要幾個關鍵參數

TeamId

你的開發者帳號 Team ID,這可以在你的右上角看到

進去 Certificates, Identifiers & Profiles -> Identifiers -> App IDs -> 您的 App -> App ID Prefix 可以看見






KeyId

您建立驗證的 Sign Key 的 Key ID

在 Certificates, Identifiers & Profiles -> Keys -> 您的 Apple Sign Key -> View Key Details -> Key ID 可以看到








PriveKey

從Keys頁面下載,只能下載一次,請好好保存

下載之後會有PriveKey,以上相關參數放入下面的c#方法,使用GetClientSecret取得client_secret

        private string GetClientSecret()
{ var signatureAlgorithm = GetEllipticCurveAlgorithm(_config.PriveKey); ECDsaSecurityKey eCDsaSecurityKey = new ECDsaSecurityKey(signatureAlgorithm) { KeyId = _config.KeyId }; var handler = new JwtSecurityTokenHandler(); var subject = new Claim("sub", _config.ClientId);//需要IOS提供 JwtSecurityToken token = handler.CreateJwtSecurityToken( issuer: _config.TeamId, audience: "https://appleid.apple.com", expires: DateTime.UtcNow.AddMinutes(5), issuedAt: DateTime.UtcNow, notBefore: DateTime.UtcNow, subject: new ClaimsIdentity(new[] { subject }), signingCredentials: new SigningCredentials(eCDsaSecurityKey, SecurityAlgorithms.EcdsaSha256)); return token.RawData; } private ECDsa GetEllipticCurveAlgorithm(string privateKey) { var keyParams = (ECPrivateKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey)); var normalizedEcPoint = keyParams.Parameters.G.Multiply(keyParams.D).Normalize(); return ECDsa.Create(new ECParameters { Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id), D = keyParams.D.ToByteArrayUnsigned(), Q = { X = normalizedEcPoint.XCoord.GetEncoded(), Y = normalizedEcPoint.YCoord.GetEncoded() } }); }

相關參數組好之後在Post到Token API就會取得id_token

id_token是一個JWT,用base64UrlDecode解開之後Payload,sub就是openId

{
  "iss": "https://appleid.apple.com",
  "aud": "com.your.app.id",
  "exp": 1596621649,
  "iat": 1596621049,
  "sub": "001451.3dc436155xxxxxxxxxxxxxxxxxxxx59f.0447",
  "c_hash": "iUqI9Vyxxxxxxxxxg-CyoA",
  "email": "8m2xxxxmew@privaterelay.appleid.com",
  "email_verified": "true",
  "is_private_email": "true",
  "auth_time": 1596621049,
  "nonce_supported": true
}

參考文件

https://blog.jks.coffee/sign-in-with-apple/

https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms

https://blog.csdn.net/Anlan2010/article/details/108419737

2021年12月10日 星期五

asp.net core cache

ASP.NET Core 的 ResponseCache 觸發伺服器端快取的條件尤為嚴格,限制很多

為了節省伺服器的運算成本,所以可以實作一個ResultFilter來快取頁面

這邊是採用本機MemoryCache,當然也可以改用Redis或者其他Cache Server

首先在Startup.cs加入services.AddMemoryCache();

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    services.AddControllers();
}

在建立一個CacheResultFilter

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication2
{
    public class CacheResultFilter : ResultFilterAttribute
    {
        private readonly int _expiration;
        public CacheResultFilter(int expirationMs)
        {
            _expiration = expirationMs;
        }
        public override void OnResultExecuting(ResultExecutingContext context)
        {            
            var controller = context.RouteData.Values["Controller"].ToString();
            var action = context.RouteData.Values["Action"].ToString();
            var method = context.HttpContext.Request.Method;
            var queryStrings = new List<string>();
            if (context.HttpContext.Request.Query.Count > 0)
            {
                context.HttpContext.Request.Query.ToList()
                   .ForEach(kv =>
                   {
                       queryStrings.Add(kv.Key);
                       queryStrings.Add(kv.Value);
                   });
            }
            var key = generateCacheKey(action, method, queryStrings.ToArray());
            var cache = context.HttpContext.RequestServices.GetService(typeof(IMemoryCache));
            if (cache == null)
            {
                base.OnResultExecuting(context);
                return;
            }
            var cacheValue = (cache as IMemoryCache)?.Get(key);
            if (cacheValue != null)
            {
                context.Result =
                    new ObjectResult(JsonConvert.DeserializeObject(cacheValue.ToString()));
            }
            else
            {
                var objResult = context.Result as ObjectResult;
                var json = JsonConvert.SerializeObject(objResult.Value);
                (cache as IMemoryCache)?.Set(key, json, TimeSpan.FromMilliseconds(_expiration));
            }
            base.OnResultExecuting(context);
        }

        private string generateCacheKey(string action,string method,string[] queryStrings)
        {
            var key = string.Format("{0}_{1}_{2}", action, method, string.Join("_", queryStrings));
            return key;
        }
    }
}

所以在Controller的這樣使用

using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace WebApplication2.Controllers { [ApiController] [Route("[controller]")] public class ExampleController : ControllerBase { [HttpGet] [CacheResultFilter(5000)] public DateTime Get() { return DateTime.Now; } } }
這邊Get掛上了CacheResultFilter設定快取5秒

取得Enum名稱

使用Enum來當作主要的Key很常見

我在很多情境上也會使用Enum的名稱來當作判斷

以下是兩種方式來取得Enum名稱

  

var name1 = ExampleType.ExampleA.ToString();

var name2 = nameof(ExampleType.ExampleA);

但是name2的寫法與name1的寫法,記憶體分配相差24倍,執行時間相差約48倍

所以還是建議使用name2的寫法,天下武功 唯快不破