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...";
        }
    }
}

2021年11月19日 星期五

狀態模式 State Pattern

脫離了博弈產業後,前往線上教育業

線上教育業有一個很固定的商業模式,成為會員前都會有一種固定的歷程

留單>預約體驗>客戶

然後有看到一段code使用狀態模式來實作

狀態模式其實跟策略模式一樣,也是要把每個實作的類別切開

只是狀態模式是可以封裝轉換規則

 
using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //留單
            var member1 = new Member
            {
                Status = 1
            };
            member1.Action();

            //預約體驗
            var member2 = new Member
            {
                Status = 2
            };
            member2.Action();

            //客戶
            var member3 = new Member
            {
                Status = 3
            };
            member3.Action();
            Console.Read();
        }
    }

    public sealed class Member
    {
        private IState current;
        public int Status { get; set; }
        public Member()
        {
            current = new 留單結構();
        }

        public void SetState(IState state)
        {
            current = state;
        }

        public void Action()
        {
            current.Process(this);
        }
    }

    public interface IState
    {
        void Process(Member member);
    }

    public sealed class 留單結構 : IState
    {
        void IState.Process(Member member)
        {
            if (member.Status == 1)
            {                
                member.SetState(new 留單實作());
                member.Action();
            }
            else
            {
                member.SetState(new 預約體驗結構());
                member.Action();
            }
        }
    }

    public sealed class 預約體驗結構 : IState
    {
        void IState.Process(Member member)
        {
            if (member.Status == 2)
            {
                
                member.SetState(new 預約體驗實作());
                member.Action();
            }
            else
            {
                member.SetState(new 客戶結構());
                member.Action();
            }
        }
    }

    public sealed class 客戶結構 : IState
    {
        void IState.Process(Member member)
        {
            if (member.Status == 3)
            {
                member.SetState(new 客戶實作());
                member.Action();
            }
            else
            {
                Console.WriteLine("查無此狀態...");
            }
        }
    }

    public class 留單實作 : IState
    {
        public void Process(Member member)
        {
            Console.WriteLine("留單狀態處理中...");
        }        
    }

    public class 預約體驗實作 : IState
    {
        public void Process(Member member)
        {
            Console.WriteLine("預約體驗處理中...");
        }
    }

    public class 客戶實作 : IState
    {
        public void Process(Member member)
        {
            Console.WriteLine("恭喜你成為我們的客戶...");
        }
    }

}


這邊會有Member context可以傳入狀態
就會依照當時的Status去實作當下狀態的類別
而實作的部分其實就是策略者模式,而多了結構類別封裝了轉換規則

2021年7月24日 星期六

c# 新增資料後寫Log

之前有寫了一篇文章EntityFrame SaveChange之後寫Log (Entity Framework 資料異動寫入Log)
但是世界沒有這麼美好,客戶想要各種的客製化Log,讓這個覆寫SaveChange不堪使用
幾乎是無用武之地,客戶已經沒這麼好騙了,幾乎每個Table新增時都要有各種花式寫Log
於是想了一個概念,如下面的Example,也不知道好不好
 
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var test1 = new Test1();
            test1.SaveChange(new TestModel
            {
                Name = "1234",
                Mail = "1234@gmial.com"
            });

            var test2 = new Test2();
            test2.SaveChange(new TestModel
            {
                Name = "5678",
                Mail = "5678@gmial.com"
            });
            Console.ReadLine();
        }

    }

    public class TestModel
    {
        public string Name { get; set; }
        public string Mail { get; set; }
    }


    public interface ITest<TModel>
    {
        int SaveChange(TModel model);
    }

    public class Test1 : TestBase, ITest<TestModel>
    {
        public int SaveChange(TestModel model)
        {
            //EF Savechange
            //不特別處理Log
            base.AddLog(model);
            return 1;
        }
    }

    public class Test2 : TestBase, ITest<TestModel>
    {
        public int SaveChange(TestModel model)
        {
            //EF Savechange
            this.AddLog(model);
            return 1;
        }

        protected override void AddLog<TModel>(TModel model)
        {
            //需要特別處理Log
            var specModel = model as TestModel;
            specModel.Name += "特別處理";
            specModel.Mail += "特別處理";
            Console.WriteLine(JsonConvert.SerializeObject(model));
        }
    }


    public class TestBase
    {
        protected virtual void AddLog<TModel>(TModel model)
        {
            Console.WriteLine(JsonConvert.SerializeObject(model));
        }
    }
}
這邊就是把SaveChange再拉出一層做一些包裝
使用者依然是無腦的使用SaveChange寫入資料
但是裡面的SaveChange會再去呼叫AddLog
每個類別可以對AddLog覆寫實作,已達到客戶的各種客製化寫Log
不知道有沒有更好的想法

2021年7月4日 星期日

c# Task 多執行緒

以前太淺再跑多執行緒都沒再限制Task的數量
所以一跑下去整台主機就卡給你看
因為把資源通通吃光了,那c#怎麼控制你想要的數量呢

使用SemaphoreSlim
 
class Program
    {
        private static readonly SemaphoreSlim Locker = new SemaphoreSlim(5);

        static async Task Main(string[] args)
        {
            List<Task> tasks = new List<Task>();
            for (int i = 0; i < 100; i++)            
                tasks.Add(Task.Run(() => TaskRun()));

            await Task.WhenAll(tasks);
        }

        public static Task TaskRun()
        {
            Locker.Wait();
            return Task.Run(async () =>
            {
                await Task.Run(() =>
                {
                    Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}_Do work.");
                    Thread.Sleep(3000);
                })
                .ConfigureAwait(false);
                Locker.Release();
            });
        }
    }

我起了100個Task因為使用SemaphoreSlim限制每次只跑5個執行緒
所以每次最多就會執行5個,這樣就不會狂跑把主機資源都吃光光

2021年6月26日 星期六

C# yield return 疊代器模式

有一個題目請印出1~100

使用for來解
 
for (int i = 1; i <= 100; i++)
{
    Console.WriteLine("Number: {0}", i);
}

這樣寫沒什麼毛病,簡單明瞭
若改成印出 1~100 之中,2 或 3 的倍數

 
for (int i = 1; i <= 100; i++)
{
    bool match = false;

    if (i % 2 == 0) match = true;
    if (i % 3 == 0) match = true;

    if (match == true)
    {
        Console.WriteLine("Number: {0}", i);
    }
}

 好像for裡面沒這麼乾淨了,若之後邏輯更複雜,可想而知後面修改的人看到這個迴圈
可能會去填離職單,所以這時候可以使用Iterator Pattern(疊代器模式)來把邏輯包起來寫法如下

 
public class Example : IEnumerator
    {
        private int _start = 1;
        private int _end = 100;
        private int _current;
        public Example(int start, int end)
        {
            this._start = start;
            this._end = end;
            this.Reset();
        }

        public object Current
        {
            get { return this._current; }
        }

        public bool MoveNext()
        {
            this._current++;
            bool match = false;            
            if (this._current % 2 == 0) match = true;
            if (this._current % 3 == 0) match = true;
            return match;
        }

        public void Reset()
        {
            this._current = 0;
        }
    }

使用方法如下
 
Example e = new Example(1, 100);
while (e.MoveNext())
{
    Console.WriteLine("Number: {0}", e.Current);
}

這樣寫的好處是你完全不需要知道實作的邏輯,你只要相信類別給你的數字顯示即可
程式碼也變得很好閱讀,也非常的乾淨,但是每次都要叫你實作Iterator Pattern
都要寫一大坨類別實作,真的也是滿累的
所以在這邊要介紹一下c#的yield return
若改成yield return會變成怎樣呢,請看以下實作

 
public static IEnumerable<int> yieldExample(int start, int end)
        {
            for (int i = 1; i <= 100; i++)
            {
                bool match = false;

                if (i % 2 == 0) match = true;
                if (i % 3 == 0) match = true;

                if (match == true)
                {
                    yield return i;
                }
            }
        }

如何使用呢
 
public void Example()
        {
            foreach (var current in yieldExample(1, 100))
            {
                Console.WriteLine("Current Number: {0}", current);
            }
        }

挖靠c#居然有這麼好用的東西

ASP.net Core 3 DI 自動註冊

在Asp.net core會開始使用DI
每次都會忘記註冊,發生錯誤才會發現,我看同事好像也很有耐心的一個一個的註冊
耐心雖然是種美德,但我個人比較懶惰,於是弄了一個DI自動註冊
先建立一個擴展

public static class NativeInjectorConfig
    {
        public static void RegisterServices(this IServiceCollection services)
        {
            //繼承ServiceBase的自動註冊
            services.RegisterInheritedTypes(typeof(ServiceBase).Assembly, typeof(ServiceBase));
        }

        public static void RegisterInheritedTypes(this IServiceCollection container, Assembly assembly, Type baseType)
        {
            var allTypes = assembly.GetTypes();
            var baseInterfaces = baseType.GetInterfaces();
            foreach (var type in allTypes)
            {
                if (type.BaseType != null && type.BaseType.GenericEq(baseType))
                {
                    var typeInterface = type.GetInterfaces().FirstOrDefault(x => !baseInterfaces.Any(bi => bi.GenericEq(x)));
                    if (typeInterface == null)
                    {
                        continue;
                    }
                    container.AddScoped(typeInterface, type);
                }
            }
        }

        public static bool GenericEq(this Type type, Type toCompare)
        {
            return type.Namespace == toCompare.Namespace && type.Name == toCompare.Name;
        }
    }


然後再到startup加入

public void ConfigureServices(IServiceCollection services)
        {    
            services.RegisterServices();            
        }

所以以上只要繼承ServerBase類別就會被偵測到自動註冊

ASP.net Core 3 Webapi 全域錯誤處理

進入了.net core時代,開始有了Middleware
所以我們可以從startup加入一個ErrrorHandler的Middleware
於是建了一個處理錯誤的class

 
  public class ErrorHandlerMiddleware
  {

        private ILogger _logger = NLog.LogManager.GetCurrentClassLogger();
        private readonly RequestDelegate _next;

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

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception error)
            {
                var response = context.Response;
                response.StatusCode = (int)HttpStatusCode.OK;
                response.ContentType = "application/json";

                var result = new ResponseBase<string>();
                result.Message = error.GetErrorMessage();
                result.StatusCode = EnumStatusCode.Fail;

                await response.WriteAsync(JsonSerializer.Serialize(result));
            }
        }
    }

之後再到startup.cs加入這一個class就可以全域處理錯誤

  
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {         
           app.UseHttpsRedirection();
            app.UseRouting();
            app.UseCors("CorsPolicy");
            app.UseAuthorization();

            app.UseMiddleware<ErrorHandlerMiddleware>();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
所以Controller發生了錯誤直接會在這邊統一處理,就不用每個Controller寫try catch 是否方便了許多