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的寫法,天下武功 唯快不破

multiple implementations of a same interface

一個Interface實作多個類別

在沒有DI的時候,都會用簡單工廠來實作,如下

using Microsoft.AspNetCore.Mvc;
using System;

using WebApplication1.Service;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ExampleController : ControllerBase
    {
        public IExample GetInstance(string className)
        {
            IExample result;
            switch (className)
            {
                case "ExampleA":
                    result = new ExampleA();
                    break;
                case "ExampleB":
                    result = new ExampleB();
                    break;
                case "ExampleC":
                    result = new ExampleC();
                    break;
                default:
                    throw new NotImplementedException();
            }
            return result;
        }


        [HttpGet]
        public string Get()
        {
            return GetInstance("ExampleA").GetName();
        }
    }
}

這種方式也沒有什麼不好,只是實作類別一多會switch很長
但有了DI之後再也不在使用Factory的方式取得實作,變成以下方式

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ExampleController : ControllerBase
    {
        private readonly IExample _example;
        public ExampleController(IEnumerable<IExample> examples)
        {
            _example = examples.Single(x => x.Type == ExampleType.ExampleA);
        }

        [HttpGet]
        public string Get()
        {
            return _example.GetName();
        }

        public enum ExampleType
        {
            ExampleA,
            ExampleB,
            ExampleC
        }

        public interface IExample
        {
            public ExampleType Type { get; }
            public string GetName();
        }

        public class ExampleA : IExample
        {
            public ExampleType Type => ExampleType.ExampleA;

            public string GetName()
            {
                return "ExampleA";
            }
        }

        public class ExampleB : IExample
        {
            public ExampleType Type => ExampleType.ExampleB;

            public string GetName()
            {
                return "ExampleB";
            }
        }

        public class ExampleC : IExample
        {
            public ExampleType Type => ExampleType.ExampleC;

            public string GetName()
            {
                return "ExampleC";
            }
        }
    }
}

Startup.cs註冊
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddScoped<IExample, ExampleA>();
    services.AddScoped<IExample, ExampleB>();
    services.AddScoped<IExample, ExampleB>();
}
之後有新增實作的話,只需要在註冊進去就可以直接使用
替換類別也非常的快速,而且關注點很清楚!!

2021年12月4日 星期六

Aspect-oriented programming(AOP)

什麼是AOP?

Aspect-oriented programming(AOP)稱為面向導向程式設計,但他不是一個設計模式
這樣說很抽象,也不知道這可以應用在何處,所以這邊講幾個可以應用的情況

1.權限的檢查 (判斷是否登入)
2.執行時間 (效能分析)
3.想記錄Method傳入的參數與傳出的參數

所以這邊介紹一個AOP套件AspectInjector,以下是程式範例
using AspectInjector.Broker;
using Newtonsoft.Json;
using System;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetData("1234"));
            Console.ReadLine();
        }

        [LogAspect]
        public static string GetData(string value)
        {
            return "test" + value;
        }       

    }

    [Aspect(Scope.Global)]
    [Injection(typeof(LogAspect))]
    public class LogAspect : Attribute
    {
        /// <summary>
        /// 執行前
        /// </summary>
        /// <param name="name">方法名</param>
        /// <param name="arguments">参数</param>
        [Advice(Kind.Before)]
        public void LogBefore([Argument(Source.Name)] string name, 
            [Argument(Source.Arguments)] object[] arguments)
        {
            Console.WriteLine($"Before,方法:'{name}'," +
                $"參數:{JsonConvert.SerializeObject(arguments)}");
        }

        /// <summary>
        /// 執行後
        /// </summary>
        /// <param name="name">方法名</param>
        /// <param name="arguments">参數</param>
        /// <param name="retrrnValue">返回值</param>
        [Advice(Kind.After)]
        public void LogAfter([Argument(Source.Name)] string name, 
            [Argument(Source.Arguments)] object[] arguments, 
            [Argument(Source.ReturnValue)] object retrrnValue)
        {
            Console.WriteLine($"After,方法:'{name}'," +
                $"參數:{JsonConvert.SerializeObject(arguments)}," +
                $"返回:{JsonConvert.SerializeObject(retrrnValue)}");
        }
    }
}

執行結果,呼叫這個Method會自動記錄Log是不是很方便







AOP最重要的目的就是,抽離共用邏輯,專注於商業邏輯

TaskCompletionSource包同步變成非同步

最近有效能問題,就是要去呼叫某個類別庫的Method

而且這些Method都沒有寫非同步,所以在要讓N個Method同時跑增快速度好像不太行

某些歷史因素要加多弄個非同步Method比較麻煩也耗時

所以一開始有想到直接用Task.Run直接包N個Method直接跑

但這樣會吃到站台的執行緒,於是去查了一下

居然有TaskCompletionSource來把同步Method變成非同步Method

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

namespace ConsoleApp2
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var task1 = GetData1Async();
            var task2 = GetData2Async();

            Console.WriteLine(await task1);
            Console.WriteLine(await task2);
            Console.ReadLine();
        }


        public static Task<string> GetData1Async()
        {
            var taskSource = new TaskCompletionSource<string>();
            var result = string.Empty;
            try
            {
                taskSource.SetResult(GetData1());
            }
            catch(Exception ex)
            {
                taskSource.SetException(ex);
            }
            return taskSource.Task;
        }

        public static Task<string> GetData2Async()
        {
            var taskSource = new TaskCompletionSource<string>();
            var result = string.Empty;
            try
            {
                taskSource.SetResult(GetData2());
            }
            catch (Exception ex)
            {
                taskSource.SetException(ex);
            }
            return taskSource.Task;
        }

        public static string GetData1()
        {
            return "GetData1...";
        }

        public static string GetData2()
        {
            return "GetData2...";
        }
    }
}