愛流浪的小風

技術隨手寫

[Tip]擴充你的Visual Studio,將常用項目包為擴充功能 (1) - 自訂新增項目

| Comments

在每個公司,都會有自己定義的程式碼撰寫規範,或是公司所開發的系統模組,而在進行這些系統模組功能開發時,若是每次都要重頭新增一個完全乾淨的Class開始進行的話,無疑是非常辛苦的一件事情,所以我們通常會將這些經常被使用的Class架構或是專案架構包成Template,再加入到Visual Studio之中,這麼一來我們就可以在開發時快速的使用我們預先定義好的Template來加快開發的進行。

但若是每次重灌電腦或是有新人進入部門時,都要重新將所有部門所常用的Template都手動設定一次,不但步驟繁瑣而且很容易出錯,最後還是要花時間來排除問題,本系列文章就是要和大家分享如何將Template包進Visual Studio的擴充功能之中,這麼一來不管是重灌電腦或是設定新環境,都只要輕鬆地安裝Visual Studio的擴充功能,就可以一次搞定所有的Template囉!

環境需求

本系列範例由Visual Studio 2013 Update 2為範例,在開始開發之前,必須先安裝Visual Studio 2013 SDK才會具有Item Template、Project Template這些樣板。

下載Visual Studio 2013 SDK

http://www.microsoft.com/en-us/download/confirmation.aspx?id=40758

定義屬於自己的項目 (Item Template)

在今天的範例中,將以如何新增AutoMapper的MappingProfile為範例,說明如何自製一個AutoMapper的MappingProfile Class項目,來減低每次都需要複製貼上的手工。

關於AutoMapper MappingProfile的使用說明,可以參考AutoMapper 的設定 (Configuration) - mrkt 的程式學習筆記

  1. 打開Visual Studio,選擇擴充性,新增一個Item Template專案

  2. 可以看到專案架構

  3. 在AutoMapper.MappingProfile.Template.vstemplate點選滑鼠右鍵,選擇屬性,將Category設為AutoMapper,這可以讓我們把這個項目分類到AutoMapper區塊

  4. 接下來要修改Class.cs檔為我們想要的樣子,先定義我們的範本架構,一個AutoMapper的Profile檔案通常會長得像這樣

    using AutoMapper;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace MyWebsiteNamespace
    {
        public class MyMappingProfile : Profile
        {
            public override string ProfileName
            {
                get
                {
                    return "MyMappingProfile";
                }
            }
    
            protected override void Configure()
            {
               //// Your mapping logic here
            }
        }
    }
    
  5. 所以我們可以修改Class.cs檔案為我們想要的樣子,並且透過預設變數來將檔名和NameSpace自動填入Class之中 (在檔案中有兩個$包住的就是變數)

    using AutoMapper;
    using System;
    using System.Collections.Generic;
    $if$ ($targetframeworkversion$ >= 3.5)using System.Linq;
    $endif$using System.Text;
    
    namespace $rootnamespace$
    {
        public class $safeitemrootname$ : AutoMapper.Profile
        {
            public override string ProfileName
            {
                get
                {
                    return "MyMappingProfile";
                }
            }
    
            protected override void Configure()
            {
                //// Your mapping logic here
            }
        }
    }
    
  6. 建置我們的專案,我們就完成了第一個自訂項目,在Debug目錄下也可以看到產生了ItemTemplate的Package

將自訂新增項目打包到擴充功能中

在完成了我們的自訂項目之後,接下來就要將自訂項目包裝到擴充功能之中了,一個擴充功能不只可以包含一個自訂項目,可以包含很多個,因次我們可以將公司中經常使用的擴充項目包裝到同一個擴充功能之中,以後就只要安裝這個擴充功能,就會一次性地得到所有的自訂項目

  1. 新增一個擴充功能專案

  2. 輸入作者、專案描述

  3. 切到Assets頁籤,點選新增,選擇Item Template以及我們的專案

  4. 存檔並建置專案,也可以直接用F5開始Visual Studio的實驗執行個體來測試剛撰寫好的擴充功能,隨意新增一個專案之後,再新增項目,就可以看到我們剛剛包好的MappingProfile樣本

  5. 產生的檔案也有自動跟著我們的檔名走,一切都很美好!

但是...由於我們新增的是AutoMapper的項目,可以看見這個專案並沒有使用Nuget加入AutoMapper的參考,所以就算新增了MappingProfile,也沒有辦法成功的建置專案,雖然可以再手動排除這些錯誤,但想讓擴充功能用起來更簡單!

讓新增項目時可以自動參考所相依的Nuget Package

  1. 打開我們的自訂項目專案,並使用Nuget安裝AutoMapper

  2. 打開AutoMapper.MappingProfile.Template.vstemplate,將專案所相依的Nuget資訊補充到最下面

    <WizardExtension>
        <Assembly>NuGet.VisualStudio.Interop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</Assembly>
        <FullClassName>NuGet.VisualStudio.TemplateWizard</FullClassName>
    </WizardExtension>
    <WizardData>
        <packages repository="extension" 
              repositoryId="VSIX Product Id">
            <package id="AutoMapper" version="3.2.1" targetFramework="net45" />
        </packages>
    </WizardData>
    

  3. 上一步驟的VSIX Product Id可以在我們的擴充功能專案中找到

  4. 在Solution資料夾的packages中找到AutoMapper的nupkg檔並複製他

  5. 在擴充功能專案中新增一個packages資料夾,並將nupkg加入到其中

  6. 設定nupkg的屬性,選擇包含在VSIX之中,並將建置動作設為內容

  7. 重新建置專案,開啟偵錯用的Visual Studio,再次的新增一個MappingProfile檔,可以看到他幫我們將Nuget的參考都自動加入了,直接建置程式也成功!

參考資料:

本日小結

在今天的文章中,我們簡單的完成了一個自訂項目的範本,並把它用擴充功能包裝起來,這麼一來我們就可以讓所有人透過安裝這個擴充功能,得到自訂項目的範本,而隨著時間的累積,我們也會擁有越來越多的自訂項目樣板,但都可以使用同一個擴充功能來包裝它,這麼一來,安裝自訂項目就是一個很輕鬆的事情囉!關於本篇文章,歡迎大家一起分享自己的作法喔 ^_^

Comments

comments powered by Disqus

[Tip]使用SourceTree同步Local和Remote的Tag

| Comments

在我們使用Git時,經常會使用Tag來對特定版本做記號,也方便我們隨時還原到該版本,但隨著時間越長,在我們Repository中的Tag也會越來越多,因此我們經常會清理遠端的Tag,只保留最近一些版本的Tag留存使用。最近發現明明已經刪除了遠端所有的Tag,重新Fetch所有的Tag之後,卻又出現在Tags列表之中,後來發現原來是我們在刪除Remote的Tags時,並不會同時刪除其他人Local的Tag,因此當他又使用SourceTree的Push all tags時,又會將所有本地端的Tag送回Remote!因此,這篇文章將向大家介紹如何透過batch script來擴充SourceTree的功能,讓本地的Tag和Remote同步,這麼一來只要在定期清理Tags時,要求大家一併同步,就不會再發生類似的問題囉!

啟用Command Line對Remote端的存取權限

由於我們平時都是直接使用SourceTree來對Remote的Repo做操作,但Custom Action的執行需要透過Command Line來進行,我們就會發現是無法使用例如git fetch --tags origin的指令的,這是由於我們尚未增加Command Line對Remote操作的權限,詳細的做法可以參考這篇文章

[Tip] Windows使用ssh對Github進行操作

設定完成之後,就可以準備來增加SourceTree的Custom Action囉!

設定SourceTree的Custom Action

  1. 首先我們先準備好同步Remote Tags的Batch Scripts,由於git並沒有提供直接的方法來同步Tags,所以在這邊我們只能使用土法煉鋼的方式,將Local端的所有Tags刪除,再重新下載Remote的Tags,將以下語法儲存為syncTags.bat

    cd %1
    
    echo 'Remove all local tags'
    
    FOR /F %%i in ('git tag -l') DO git tag -d %%i
    
    echo 'Fetch tags from origin'
    
    git fetch --tags origin
    
  2. 開啟SourceTree,選擇Tools => Options

  3. 點選Add,新增一個Custom Action

  4. 輸入名稱Sync Tags,選擇剛剛儲存的bat檔位置,帶入參數$REPO並點選OK

  5. 之後我們就可以直接從選單上使用Sync Tags功能囉!

  6. 執行後我們可以看到正確的重新下載Tags

本日小結

SourceTree是一套非常好用的Git GUI工具,它可以讓就算不熟悉git的人也可以很快地透過圖形化介面上手,而透過增加一些Custom Action,還可以讓我們使用外部工具來開啟檔案,或執行某些Script,節省我們的時間!關於今天的內容如果有問題歡迎一起討論喔^_^

Comments

comments powered by Disqus

[PoC系列] 如何使用非同步更新網站快取(Cache)

| Comments

在網站系統中,我們經常會使用Cache來加快網站的回應速度,而如何讓Cache使用的效益最佳化也是一個很值得討論的議題,今天的文章主要是實作一些抽象的想法,並不一定可以直接套用在正式的網站之中,也歡迎大家一起分享自己的意見,未來也會撰寫一些類似系列的文章,來實作一些有趣的想法。

如果我們網站的某些頁面流量比較大,而且會進行較大量的資料庫操作,或是使用CPU來做高度的運算,但可以接受使用者不一定需要看到最新的資料,並允許短暫時間的誤差 (例如: 五分鐘),這時候我們就可以考慮在這個頁面上加上Cache來減低Server的負擔。最常應用Cache的場景應該就是網站的首頁了,網站首頁通常會包含大量的資訊,也擁有最大量的瀏覽量,但可以容許資料十分鐘才更新一次,正是適合Cache的使用。今天我們也將舉一個簡單的範例來實作Cache,並逐步的調整使用的方法!

模擬一個需要較長時間回應的頁面

首先我們實作一個頁面來呈現一個清單,但為了模擬長時間的運算,所以撰寫Service如下,在執行時會Delay5秒鐘才回傳資料

    public interface ILongRunningService
    {
        CacheDto<IEnumerable<int>> GetData(int min, int max);
    }

    public class LongRunningService : ILongRunningService
    {
        public CacheDto<IEnumerable<int>> GetData(int min, int max)
        {
            Thread.Sleep(5000);

            var r = new Random();
            var result = new List<int>();

            for (int i = 0; i < 10; i++)
            {
                result.Add(r.Next(min, max));
            }

            return new CacheDto<IEnumerable<int>>
            {
                Data = result,
                UpdateTime = DateTime.Now
            };
        }
    }

CacheDto主要作為資料交換使用,並可以顯示資料被更新的時間

    public class CacheDto<T>
    {
        public T Data { get; set; }

        public DateTime UpdateTime { get; set; }
    }

    public class CacheDto: CacheDto<object>
    {
    }

將內容呈現在網頁上

你可以從Github tag: Basic website取得網站雛形

參考資料:

使用Redis做為網站的快取服務

Redis為一個KeyValue Pair類型的資料庫,也經常被使用作為Cache的場景,現在Azure之中也有提供Azure Redis Cache的服務,雖然目前是Preview版本,但我想應該不需要太久正式版就會出現在大家面前,本篇的範例也將使用Redis作為Cache的Server來儲存資料,接下來就向大家介紹如何將資料存放在Redis之中

  1. 安裝Redis服務,可以直接在Azure上申請,或是使用nuget下載

    Install-Package Redis-64
    

    使用Nuget下載後,直接執行redis-server.exe即可

  2. 安裝Redis Client,我們使用StackExchange.Redis作為Redis Client

    Install-Package StackExchange.Redis
    
  3. 在Autofac中註冊Redis的連線,並設定為Singleton (官方文件建議保留Connection,重複使用)

    builder.Register(i =>
    {
        var connect = ConnectionMultiplexer.Connect("localhost");
    
        return connect;
    }).AsSelf()
    .SingleInstance();      
    
  4. 新增一個StackExchangeRedisExtension的Class,擴充IDatabase介面,為設定和取得提供泛型及自動序列化的方法 (參考自MSDN)

    public static class StackExchangeRedisExtension
    {
        public static T Get<T>(this IDatabase cache, string key)
        {
            return Deserialize<T>(cache.StringGet(key));
        }
    
        public static object Get(this IDatabase cache, string key)
        {
            return Deserialize<object>(cache.StringGet(key));
        }
    
        public static void Set(this IDatabase cache, string key, object value, TimeSpan? expire = null)
        {
            cache.StringSet(key, Serialize(value), expire);
        }
    
        static byte[] Serialize(object o)
        {
            if (o == null)
            {
                return null;
            }
    
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream())
            {
                binaryFormatter.Serialize(memoryStream, o);
                byte[] objectDataAsStream = memoryStream.ToArray();
                return objectDataAsStream;
            }
        }
    
        static T Deserialize<T>(byte[] stream)
        {
            if (stream == null)
            {
                return default(T);
            }
    
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream(stream))
            {
                T result = (T)binaryFormatter.Deserialize(memoryStream);
                return result;
            }
        }
    }
    
  5. 改寫LongRunningService,改為透過Cache取得資料,若沒有資料才直接產生,並寫入Cache中

    public class LongRunningService : ILongRunningService
    {
        public ConnectionMultiplexer RedisConnection { get; set; }
    
        public LongRunningService(ConnectionMultiplexer connection)
        {
            this.RedisConnection = connection;
        }
    
        public CacheDto<IEnumerable<int>> GetData(int min, int max)
        {
            var cache = this.RedisConnection.GetDatabase();
    
            var key = string.Format("LongRunningService.GetData.{0}.{1}", min, max);
            var cachedData = cache.Get<CacheDto<IEnumerable<int>>>(key);
            if (cachedData == null)
            {
                Thread.Sleep(5000);
    
                var r = new Random();
                var result = new List<int>();
    
                for (int i = 0; i < 10; i++)
                {
                    result.Add(r.Next(min, max));
                }
    
                cachedData = new CacheDto<IEnumerable<int>>
                {
                    Data = result,
                    UpdateTime = DateTime.Now
                };
    
                cache.Set(key, cachedData, TimeSpan.FromSeconds(30));
            }
    
            return cachedData;
        }
    }
    
  6. 記得將CacheDto加上Serializable的Attribute,標記為可序列化

    [Serializable]
    public class CacheDto<T>
    {
        public T Data { get; set; }
    
        public DateTime UpdateTime { get; set; }
    }
    
  7. 重新整理頁面,可以發現在30秒內所取得的資料都會一模一樣,更新時間也相同,30秒之後才會更新,代表我們的Cache成功生效了!完成的程式碼請參考Github Branch: Integrate with redis

參考資料:

使用AOP,讓程式碼更加的乾淨!

在上一個段落中,我們整合了Redis作為我們網站的Cache,避免每次都需要重新產生資料,也減低了Server的負擔,但我們發現如果直接在程式碼每一段需要的地方都加上Cache的程式碼的話,不僅僅讓每一個Class的商業邏輯都變複雜,在測試上也不容易進行,因此我們可以透過Aop的方法來重構,撰寫通用的Inteceptor來實作Cache的機制,讓Cache的程式碼獨立在商業邏輯之外,只需要再要套用的Function上加上Attribute即可!

  1. 安裝Autofac.Extras.DynamicProxy2,支援Autofac的Aop套件

    Install-Package Autofac.Extras.DynamicProxy2
    
  2. 建立CacheAttribute,並可指定Cache持續時間 (單位: 秒)

    由於Autofac的Aop只能針對Class指定,但我們不一定這個Class的每個Method都希望被Cache,所以我們額外撰寫一個Attribute加在Method上,用來判斷這個Method是否需啟用快取,有加的才啟用。

    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    sealed class CacheAttribute : Attribute
    {
        public CacheAttribute()
        {
            this.ExpireTime = 20 * 60;
        }
    
        public string CacheKey { get; set; }
    
        public int ExpireTime { get; set; }
    }
    
  3. 建立CacheInterceptor,用來共用的Cache邏輯

    public class CacheInterceptor : IInterceptor
    {
        public ConnectionMultiplexer RedisConnection { get; set; }
    
        public CacheInterceptor(ConnectionMultiplexer connection)
        {
            this.RedisConnection = connection;
        }
    
        public void Intercept(IInvocation invocation)
        {
            //// 判斷目前方法是否有需要啟用Cache,若有加上標記表示需要
            var attributes = invocation.MethodInvocationTarget.GetCustomAttributes(typeof(CacheAttribute), true);
            if (attributes.Count() > 0)
            {
                //// Cache key
                var key = string.Format("{0}.{1}.{2}", invocation.TargetType.FullName,
                                                       invocation.MethodInvocationTarget.Name,
                                                       JsonConvert.SerializeObject(invocation.Arguments));
    
                //// Expire Time
                var expireTime = (attributes.First() as CacheAttribute).ExpireTime;
    
                IDatabase cache = this.RedisConnection.GetDatabase();
    
                //// Cache是否存在,不存在產生新的
                var result = cache.Get(key);
                if (result != null)
                {
                    invocation.ReturnValue = result;
                    return;
                }
    
                invocation.Proceed();                
    
                cache.Set(key, invocation.ReturnValue, TimeSpan.FromSeconds(expireTime));
            }
        }
    }
    
  4. 在LongRunningService上,加上Cache的Attribute

    [Intercept(typeof(CacheInterceptor))]
    public class LongRunningService : ILongRunningService
    {
        [Cache(ExpireTime = 30)]
        public CacheDto<IEnumerable<int>> GetData(int min, int max)
        {
            Thread.Sleep(5000);
    
            var r = new Random();
            var result = new List<int>();
    
            for (int i = 0; i < 10; i++)
            {
                result.Add(r.Next(min, max));
            }
    
            return new CacheDto<IEnumerable<int>>
            {
                Data = result,
                UpdateTime = DateTime.Now
            };
        }
    }
    
  5. 在Autofac的註冊中,標記LongRunningService需要注入攔截器,並且註冊CacheInterceptor

    builder.RegisterType<LongRunningService>()
           .As<ILongRunningService>()
           .EnableInterfaceInterceptors();
    
    builder.RegisterType<CacheInterceptor>()
           .AsSelf();       
    
  6. 重新執行網頁,可以看到網頁按照我們預期的,每30秒更新一次資料,完成的程式碼請參考Github Branch: Integrate with redis and aop

參考資料:

使用HangFire,將更新Cache的工作交給背景來執行

在上面的段落中,基本上已經完成了一個簡單的Cache機制雛形,但有沒有可能再更進一步呢? 如果我們可以不要在前端的程式中更新頁面,而是當Cache過期時,新增一個背景工作,交給background的Queue System來更新Cache,這麼一來,我們就可以保持前端的頁面永遠是非常快地回應給Client!接下來我就要向大家介紹如何使用HangFire這套Framework來執行背景的工作。

  1. HangFire是一套Queue Framework,可以讓我們在背景執行指定的工作,而且非常簡單就能套用,我們先來進行HangFire的安裝

    Install-Package HangFire
    Install-Package HangFire.Redis
    Install-Package HangFire.Autofac
    
  2. 修改自動產生HangFireConfig檔案,使用Redis作為儲存空間

    public class HangFireConfig
    {
        private static AspNetBackgroundJobServer _server;
    
        public static void Start()
        {                
             JobStorage.Current = new RedisStorage("localhost:6379", 3);                              
    
            _server = new AspNetBackgroundJobServer();
            _server.Start();
        }
    
        public static void Stop()
        {
            _server.Stop();
        }
    }
    
  3. 連到http://localhost:1234/hangfire.axd,可以看到管理介面就代表安裝成功

  4. 新增一個RenewCacheAdapter,讓我們可以在背景中更新Cache,這邊主要是透過反射的機制,來動態執行撈取資料的Class取得資料並更新Cache

    public class RenewCacheAdapter
    {
        public ILifetimeScope LifetimeScope { get; set; }
    
        public ConnectionMultiplexer RedisConnection { get; set; }
    
        public RenewCacheAdapter(ILifetimeScope lifetimeScope, ConnectionMultiplexer connection)
        {
            this.LifetimeScope = lifetimeScope;
            this.RedisConnection = connection;
        }
    
        public void RenewCache(string key, int expireTime, string typeName, string methodName, string argumentString)
        {
            IDatabase cache = this.RedisConnection.GetDatabase();
    
            //// 若未過期,不執行更新
            var result = cache.Get<CacheDto>(key);
            if (result.UpdateTime + TimeSpan.FromSeconds(expireTime) > DateTime.Now)
            {
                return;
            }
    
            //// 取得資料來源Class
            var targetType = Type.GetType(typeName);
            var methodInfo = targetType.GetMethod(methodName);
    
            var target = this.LifetimeScope.Resolve(targetType);
    
            //// 將參數轉換回原本的Type (因Json.Net會預設反序列化為Int64,但此處為Int32)
            var arguments = JsonConvert.DeserializeObject<object[]>(argumentString);
            var parameterInfos = methodInfo.GetParameters();
            var changeTypesArguments = new List<object>();
            for (int i = 0; i < parameterInfos.Length; i++)
            {
                var argument = arguments[i];
                var parameterInfo = parameterInfos[i];
    
                var changedArgument = Convert.ChangeType(argument, parameterInfo.ParameterType);
    
                changeTypesArguments.Add(changedArgument);
            }
    
            //// 執行方法,取得更新後的資料
            var returnValue = methodInfo.Invoke(target, changeTypesArguments.ToArray());
    
            result = new CacheDto
            {
                Data = returnValue,
                UpdateTime = DateTime.Now
            };
    
            //// 最長Cache時間,一小時 (避免Hangfire過於忙碌時,資料都沒有更新)
            cache.Set(key, result, TimeSpan.FromHours(1));
        }
    }
    
  5. 修改我們原本的CacheInterceptor,當Cache過期時,用寫入一個HangFire的更新Cache Job來取代直接更新Cache

    public class CacheInterceptor : IInterceptor
    {
        public ConnectionMultiplexer RedisConnection { get; set; }
    
        public CacheInterceptor(ConnectionMultiplexer connection)
        {
            this.RedisConnection = connection;
        }
    
        public void Intercept(IInvocation invocation)
        {
            //// 判斷目前方法是否有需要啟用Cache,若有加上標記表示需要
            var attributes = invocation.MethodInvocationTarget.GetCustomAttributes(typeof(CacheAttribute), true);
            if (attributes.Count() > 0)
            {
                //// Cache key
                var key = string.Format("{0}.{1}.{2}", invocation.TargetType.FullName,
                                                       invocation.MethodInvocationTarget.Name,
                                                       JsonConvert.SerializeObject(invocation.Arguments));
    
                //// Expire Time
                var expireTime = (attributes.First() as CacheAttribute).ExpireTime;
    
                IDatabase cache = this.RedisConnection.GetDatabase();
    
                //// 完全無資料時,直接產生
                var result = cache.Get<CacheDto>(key);
                if (result == null)
                {
                    invocation.Proceed();
    
                    var dataToCache = new CacheDto{
                        Data = invocation.ReturnValue,
                        UpdateTime = DateTime.Now
                    };
    
                    cache.Set(key, dataToCache, TimeSpan.FromHours(1));
    
                    return;
                }
    
                //// 快取過期時,產生一個背景更新的工作
                //// 因背景後續才執行,因此此處可以馬上回應
                if (result.UpdateTime + TimeSpan.FromSeconds(expireTime) < DateTime.Now)
                {
                    BackgroundJob.Enqueue<RenewCacheAdapter>(
                        i => i.RenewCache(key,
                                          expireTime,
                                          invocation.TargetType.FullName,
                                          invocation.MethodInvocationTarget.Name,
                                          JsonConvert.SerializeObject(invocation.Arguments)));
                }
    
                //// 直接回傳舊資料
                invocation.ReturnValue = result.Data;
            }
        }
    }
    
  6. 最後在AutofacConfig中註冊所需要的Class

    //// For website
    builder.RegisterType<LongRunningService>()
           .As<ILongRunningService>()
           .EnableInterfaceInterceptors();
    
    //// For background job
    builder.RegisterType<LongRunningService>()
           .AsSelf();
    
    builder.RegisterType<CacheInterceptor>()
           .AsSelf();
    
    builder.RegisterType<RenewCacheAdapter>()
           .AsSelf();
    
  7. 重新執行程式,我們可以發現不論是否更新資料,每次的回應都相當快速,而在背景中也可以看到有任務被寫入,並使用HangFire執行進行Cache的更新

完成的程式碼請參考Github Branch: Integrate with hangfire

參考資料:

總結

本篇文章只是一個概念的雛型,若要真正實際應用到網站之中還有許多可以完善調整的地方,但可藉此來對一些想法做初步的驗證,也是非常的有趣,未來有機會還會分享更多類似的內容,希望可以和大家一起腦力激盪來討論一些有趣架構的實作,也歡迎大家分享自己的想法喔 ^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (30) - 總結

| Comments

在過去30天中,我們實際透過逐步完成Api的功能,熟悉Asp.Net MVC的結構和擴充的方法,也實際發行到Azure上,並透過Azure所提供的服務來完成我們的工作,也介紹了如何監控和管理線上網站的狀態,其實這些方法並不僅僅能用在Api系統上,有絕大部分的經驗就算是使用在一般網站站台的開發上也是沒有問題的,大家可以依據自己的需求來套用到自己的系統開發之中,今天也將向大家分享在這30天的文章分享的一些心得總結。

使用Gitflow作為開發流程控制

在這30天的文章分享之中,每天在文章中所Demo的程式碼其實也是當天所產出的,進度非常的刺激,除了是第一次完全採用Git當作版本控制系統之外,還選擇使用Gitflow這套開發流程來幫助開發。

在最早期開始工作時,所使用的版本控制系統為CVS和SVN,在其中也有用過VSS。認識git之前其實都還沒有什麼branch的觀念,而使用git之前最痛苦的時候就是有多個版本要簽入同一支程式碼,但是不是同時要Release,常常會造成最後版本錯亂,真是相當的可怕。而使用Git之後也慢慢的愛上了它,讓我可以更放心的新增分支去做修改,確認無誤在Merge回要Deploy的分支即可。

這次我還使用了gitflow這套工具,在經歷過剛開始的不熟悉之後,也愛上的Gitflow,因為它就是一套開發流程的原則,讓我很清楚的知道我甚麼時候應該將分支合併回主幹(master),而我在撰寫新功能時隨時都可以很輕鬆的作破壞性的嘗試,若嘗試失敗也只要切回原本的brance就可以馬上還原為一個乾淨的環境,這對快速開發來說幫助相當的大,非常推薦給大家使用。

DI Framework的導入

大家可以看到我在文章一開始的時候就介紹了如何使用DI Framework來當作系統的核心,透過它來幫助我們初始化各個實體,對大部分人來說DI Framework在系統開發上來說是較為困難而且複雜的,是不是有這個必要性,但我覺得系統在開發最重要的一件事情就是在一開始建立好對的原則,而且透過它我可以讓系統程式碼的可維護性大大提高。

使用DI Framework的好處是,讓Class之間的互相引用要盡量透過介面,不要直接參考實體,若是使用DI Framework,我們可以透過DI Framework來自動注入對應的實體,如果沒有選擇使用DI Framework,也不要直接用new instance的方式,而是盡量使用Factory Pattern來初始化實體。

這些方法可以讓Class之間的耦合性降低,因為所有的引用都是透過介面,所以當我們要修改或升級其中某一個Class的功能時,也不會因為Class之間關連過多而難以著手。除此之外也方便單元測試的進行,因為我們可以透過介面來Mock各種情形,減少Side Effect的產生,撰寫單元測試所省下來的成本絕對比想像中的還要高。

模組化的程式碼

透過這段時間的開發,大家可以發現我們在Api中的程式碼邏輯都是一小塊一小塊區分開來的,而每一塊都有自己的職責和功用,這是因為在開發時期遵循了關注點分離(Seperated of Concern)的精神,也符合了OO中的單一職責原則,讓每一個Class都只實現自己主要負責的工作,不會包含過多的其他資訊,而每一個功能群組的Class也會分類在同一區之中,這樣子可以讓我們在進行開發或是單元測試是不會面對過多的雜訊干擾,而是可以專注在自己想要完成的功能上面,雖然程式碼的數量變多了,但是由於分類和存放的位子都有一定的規則在,只要習慣後在維護上反而更加的快速,因為修改時不會一次要面對四五種邏輯在一個Function之中,而是一次只要把一件事情做好就OK了!

如果我們在開發時讓程式碼專注在單一功能上,其實也無形的讓程式的可重用性變得更高,相信大家都在實際開發時遇過這種狀況,我們有一個功能可能會用到某一個Class的80%程式碼,但另外20%的實在是不需要,有些人會選擇是再寫一個新的Class然後有80%的程式碼重複,亦或是增加一個參數,然後再用if來避開,但這兩種做法都是不太好的,可能會增加維護時的困難性,相反的如果我們可以修改原本Class的邏輯切成更小的單元,那麼就可以讓同一隻Class使用在更多地方,但如果一次就將程式碼切為過於零碎就是一種過度設計了。

我們應該依照敏捷開發的精神,在遵循基本OO原則的前提下快速開發,並同時撰寫單元測試,配合版本控制系統與CI Server來管理發行程式。而當有需求異動時,我們才會調整不合理的地方,並讓測試通過,透過CI Server的自動化建置和測試確保程式都處在Avaliable的狀態,就像Ruddy老師曾經說過敏捷開發的精神是擁抱改變,不要害怕變化,而是透過完善的機制讓我們能夠快速因應變化!

善用Open Source的Library

在這一系列的文章中,可以看到我選擇使用了Open Source的Library來完成功能,尤其現在Open Source的社群非常活躍,我們不一定每次都需要重新造輪子,可以選擇使用功能相對完善的Open Source Library,讓我們在開發時節省大量的時間。

很多人可能會擔心使用外部的Library似乎不如自己開發的可靠,但其實活躍的Library使用者可能是成千上萬人,遇到問題時也可以很輕易的在網路上找到答案,如果真的有使用上的問題,擁有熱血工程師靈魂的你正式一展長才的好機會,還能將它修正並且回饋給社群,為Open Source Library貢獻自己的一份力量,讓這些Library更加完善,也是一種工程師的浪漫。當然切記使用Open Source Library時也不可忽略授權的問題囉!

本日小結

其實這30天過得很刺激,在這系列之前所做的準備是有一個Api的構思與雛形,想像了大概30天要寫的題目就開始動筆了,接下來30天果不其然就是每天都在實驗想法是否可行,以及如何實現到自己的Api中(幸好有Git!),同樣的事情卻又不能因為完成當天的進度也休息,因為明天的程式碼也都還沒驗證過需要擔心(就像是熱血的趕了30天專案)

但這30天也是有很大的收穫,不但好好的磨練了自己與Git Flow的熟悉度,也和Azure有了初步的認識,對於自己的學習成長有很大的幫助,今年要反省的老毛病是對於文章的撰寫還是不夠熟練和流暢(寫一天不會感覺,寫了三十天自己都會搖頭XD),以及在內容性的完整度上還需要再努力,尤其是細節不可以太過於鬆散而忽略,希望自己能夠在努力加油囉!

今天是最後一篇了,希望這些文章能夠對大家有幫助,也歡迎大家一起討論囉^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (29) - 使用HipChat整合系統通知

| Comments

在昨天的文章之中,介紹了如何透過每日錯誤統計報表來觀察網站的健康狀況,然而除了每天的報表之外,很多時候我們希望可以即時的得知網站目前的情形,但又不希望花費太多的人力在線上網站的觀察上面,這時候我們一樣也可以透過排程工去定期檢查我們希望監控的資料,當有異常時發送出通知給我們的方式來監控線上網站。

安裝並建立HipChat聊天室

其實最早的時候系統通知也是使用發送Email的方式,但長期使用下來發現透過Email通常只會產生大量的郵件,相當的干擾正常郵件的收發,這時候剛好看到XDite大大分享的把公司 Log 搬到 Hipchat,就開始嘗試使用這套軟體來整合系統通知(5人以下免費),而這套工具除了提供Web的介面之外,幾乎各平台都有Client(包含iOS, Android),所以就當作系統通知的選擇,首先我們先來申請並安裝HipChat。

  1. 進入HipChat官網,申請一個帳號

  2. 申請完成之後可以看到聊天室畫面,記得去收驗證信

  3. 建立一個聊天室存放系統通知

  4. 建立聊天室成功

延伸閱讀:

整合Github通知

現在有非常多的版本控制服務,比較出名的有Github、Bitbucket等等,而這兩間也都有提供Service Hook整合HipChat的功能,可以讓我們在推送Branch等行為時發送通知到Hipchat中,接下來就來示範如何啟用通知

  1. 進入HipChat的Group Admin頁面,點選Api頁籤,產生一組新的API Token,並記下Token

  2. 到Github專案頁面點選Settings

  3. 選擇Service Hooks,找到HipChat,並填入Token等資料

  4. 實際操作Branch,可以看到若對Remote有變更時會在HipChat出現資訊

如此一來當多人同時開發時,我們就可以觀察到有沒有人進行新的Commit!

整合TFS建置通知

除了希望在版本控制系統中發送通知之外,我們也希望可以在TFS建置完成時發送通知到HipChat中,這麼一來若建置失敗我們就可以第一時間的處理它。

  1. 進入TFS專案首頁,點選右上角進入Admin頁面

  2. 點選Alerts頁籤,選擇Build Alerts,新增一個Alert

  3. 設定使用SOAP方法通知,並設定通知網址,這邊我們透過HipChat的API來通知,使用SOAP方法,建置成功時觸發通知

  4. 通知網址中包含了我們想要通知訊息的內容以及格式

    https://api.hipchat.com/v1/rooms/message?room_id={roomid{&from=TFS&message=Project+ApiSample+Build+Success!&auth_token={token}&color=green
    
  5. 我們可以同樣的新增建置錯誤通知,但設定不同顏色來做區別

  6. 從Visual Studio觸發一個新的建置

  7. 建置成功後我們可以在HipChat收到訊息

透過CI Server的建置通知,我們可以很快的在第一時間發現版本控制系統中的程式碼有誤,並且馬上排除,這可以讓我們的程式碼一直維持在高可用性的狀態!

延伸閱讀:

偵測Elmah錯誤過多時通知

除了監測版本控制系統和CI Server之外,我們也很常需要觀察線上伺服器的健康狀況,如果線上網站瞬間Exception過多時,一定是網站有潛在的問題存在,這邊就將向大家介紹如何設定一個工作排程來監控線上網站的錯誤狀況

  1. 在原本我們新增排程的Worker Role中使用Nuget安裝HipChat.Net

  2. 增加一個HipChatNotifier,用來發送錯誤訊息到HipChat

    public interface INotifier
    {
        void Notice(string message);
    
        void Alert(string message);
    
        void Error(string message);
    }
    
    public class HipChatNotifier : INotifier
    {
        public string Token { get; set; }
    
        public string RoomId { get; set; }
    
        public string SenderName { get; set; }
    
        public HipChatClient Client { get; set; }
    
        public HipChatNotifier(string token, string roomId, string sender)
        {
            this.Token = token;
            this.RoomId = roomId;
            this.SenderName = sender;
    
            this.Client = new HipChatClient(token, roomId, sender);
        }       
    
        public void Notice(string message)
        {
            this.Client.SendMessage(message, HipChatClient.BackgroundColor.green);
        }
    
        public void Alert(string message)
        {
            this.Client.SendMessage(message, HipChatClient.BackgroundColor.yellow);
        }
    
        public void Error(string message)
        {
            this.Client.SendMessage(message, HipChatClient.BackgroundColor.red);
        }
    }
    
  3. 增加一個排程工作,檢查資料庫中十分鐘內是否有過多錯誤,大家可以依據自己的需求調整參數

    public class ExceptionMonitorJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            var startTime = DateTime.UtcNow.AddMinutes(-10);
            var endTime = DateTime.UtcNow;
    
            var threshold = 10;
            var client = new HipChatNotifier("[token]", "[room id]", "ExceptionMonitor");
    
            using (ExceptionDBContext dbContext = new ExceptionDBContext())
            {
                var errors = dbContext.ELMAH_Error
                                     .Where(
                                        i => i.TimeUtc > startTime &&
                                           i.TimeUtc <= endTime)
                                     .GroupBy(i => i.Application)
                                     .Select(
                                        i =>
                                        new
                                        {
                                            Name = i.Key,
                                            Count = i.Count()
                                        }
                                      );
    
                foreach (var error in errors)
                {
                    if (error.Count > threshold)
                    {
                        client.Alert(string.Format("{0}'s error count is {1} between {2} and {3}", error.Name, error.Count, startTime.ToLocalTime(), endTime.ToLocalTime()));
                    }
                }
            }
        }
    }
    
  4. 新增排程工作到Worker Role中

    var job = JobBuilder.Create<ExceptionMonitorJob>()
                        .WithIdentity("DailyExceptionJob", null)
                        .Build();           
    
    ITrigger trigger = TriggerBuilder.Create()
                                     .WithIdentity("default", null)
                                     .StartAt(runTime)
                                     .WithCronSchedule("1 * * * * ?")
                                     .Build();
    
    scheduler.ScheduleJob(job, trigger);
    
  5. 當錯誤過多時可以看到HipChat收到通知

這麼一來就算沒有時時刻刻觀察線上錯誤訊息,但萬一有錯誤訊息爆增的時候我們也能馬上收到警告並即時處理了!

本日小結

我們在整合了HipChat之後,可以將許多的通知訊息都使用HipChat來接收呈現,它還有一個好處是會保留所有訊息,就算發送訊息時你並未登入,也可以在上線之後檢視訊息內容,不會遺漏,我們還可以再依據自己的使用需求繼續去掛載其它通知到HipChat中,關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (28) - 管理網站錯誤資料

| Comments

在之前的文章中,有介紹過如何使用Elmah來記錄與收集網站的錯誤資訊,而且Elmah也提供了十分好用的錯誤瀏覽介面,可以讓我們觀察線上網站的錯誤記錄,然而在實際營運網站時比較尷尬的是,線上網站發生錯誤的頻率是相當高的,就算過濾掉404等還是相當可觀,我們不可能無時無刻都在觀察Elmah的網頁,但將錯誤通知打開通知訊息又會過多,相當令人困擾,然而我們也不可能放著這些錯誤訊息不修正,因此有一個有效的錯誤訊息管理方法也是很重要的,今天將向大家分享如何管理網站的錯誤訊息。

建立每日錯誤統計報表

為了有效率的管理網站的錯誤訊息,我們可以每天統計各種錯誤發生的次數,再根據目前的專案進度或是工作內容,將發生頻率最高或影響最大的錯誤排入日常處理,而我們在之前將錯誤記錄存放在SQL Server中也是為了可以方便進行統計分析。

  1. 在Visual Studio的Cloud Service專案中,建立一個背景工作角色

  2. 新增一個背景工作角色

  3. 新增一個實體資料模型,用來存取Elmah的資料庫

  4. 設定連線字串

  5. 選擇所要加入的資料表

  6. 新增錯誤資料模型,用來存放及呈現錯誤資料

    public class ErrorReportModel
    {
        public string ApplicationName { get; set; }
    
        public string ExceptionType { get; set; }
    
        public string ExceptionMessage { get; set; }
    
        public int ExceptionCount { get; set; }
    }
    
  7. 新增報表範本,並且修改屬性為內嵌資源,我們在這邊使用RazorEngine這套Library來幫助我們完成Email的內容,它可以解析Razor格式的樣板並且將資料代入執行

    @foreach(var reports in Model)
    {
        <h2>@reports.Key</h2>
        <table border="1">
            <thead>
                <tr>
                    <th>錯誤類型</th>
                    <th>錯誤訊息</th>
                    <th>錯誤次數</th>
                </tr>
            </thead>
            <tbody>
                @foreach(var report in reports.Value)
                    {
                        <tr>
                            <td>@report.ExceptionType</td>
                            <td>@report.ExceptionMessage</td>
                            <td>@report.ExceptionCount</td>
                        </tr>
                }
            </tbody>
        </table>
    }
    
  8. 新增一個排程工作,根據每天產生的錯誤進行分類,執行完成後送出報表 (使用Quartz.Net)

    public class DailyExceptionReportJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            // 決定開始和結束時間
            DateTime startTime = DateTime.Today.AddDays(-1).ToUniversalTime();
            DateTime endTime = DateTime.Today.ToUniversalTime();
    
            // 取得每日錯誤資料
            List<ErrorReportModel> result = this.GetErrorsByDatetimeRange(startTime, endTime);
    
            // 產生統計後資料
            Dictionary<string, List<ErrorReportModel>> reports = this.GetReportModels(result);
    
            // 產生HTML報表
            string content = this.GetReportHtml(reports);
    
            //寄送每日報表
            this.MailReport(content);            
        }
    
        private void MailReport(string content)
        {
            MailAddress from = new MailAddress("xxx@email.com", "xxxx", System.Text.Encoding.UTF8);
            MailMessage mail = new MailMessage(from, new MailAddress("xxx@email.com"));
    
            string subject = "Daily Error Report";
            mail.Subject = subject;
            mail.SubjectEncoding = System.Text.Encoding.UTF8;
    
            string body = content;
            mail.Body = body;
            mail.BodyEncoding = System.Text.Encoding.UTF8;
            mail.IsBodyHtml = true;
            mail.Priority = MailPriority.High;
    
            SmtpClient client = new SmtpClient();
            client.Host = "smtp.gmail.com";
            client.Port = 587;
            client.Credentials = new NetworkCredential("xxx@email.com", "xxxx");
            client.EnableSsl = true;
    
            client.Send(mail);
        }
    
        private string GetReportHtml(Dictionary<string, List<ErrorReportModel>> reports)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            Stream str = assembly.GetManifestResourceStream("Ap iSample.BatchJob.BL.DailyException.ErrorReportTemplate.cshtml");
            StreamReader sr = new StreamReader(str, System.Text.Encoding.UTF8);
            var template = sr.ReadToEnd();
            string content = Razor.Parse(template, reports);
            return content;
        }
    
        private Dictionary<string, List<ErrorReportModel>> GetReportModels(List<ErrorReportModel> result)
        {
            Dictionary<string, List<ErrorReportModel>> reports = new Dictionary<string, List    <ErrorReportModel>>();
            foreach (var applicationErrors in result.GroupBy(i => i.ApplicationName))
            {
                var report = applicationErrors.GroupBy(i => i.ExceptionType)
                                              .Select(
                                                i => new ErrorReportModel
                                                {
                                                    ApplicationName = applicationErrors.Key,
                                                    ExceptionType = i.Key,
                                                    ExceptionMessage = i.First().ExceptionMessage,
                                                    ExceptionCount = i.Count()
                                                })
                                              .OrderByDescending(i => i.ExceptionCount)
                                              .ToList();
    
                reports.Add(applicationErrors.Key, report);
            }
            return reports;
        }
    
        private List<ErrorReportModel> GetErrorsByDatetimeRange(DateTime startTime, DateTime endTime)
        {
            List<ErrorReportModel> result;
            using (ExceptionDBContext dbContext = new ExceptionDBContext())
            {
                result = dbContext.ELMAH_Error
                                  .Where(
                                      i => i.TimeUtc >= startTime &&
                                           i.TimeUtc < endTime)
                                  .Select(
                                      i => new ErrorReportModel()
                                      {
                                          ApplicationName = i.Application,
                                          ExceptionType = i.Type,
                                          ExceptionMessage = i.Message
                                      })
                                  .ToList();
            }
    
            return result;
        }
    }
    
  9. 在WorkerRole中設定工作排程,每天執行一次

    public class WorkerRole : RoleEntryPoint
    {
        private IScheduler scheduler;
    
        private ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
        public override void Run()
        {
            DateTimeOffset runTime = DateBuilder.EvenMinuteDate(DateTime.UtcNow);
            DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 10);
    
            var job = JobBuilder.Create<DailyExceptionReportJob>()
                .WithIdentity("DailyExceptionJob", null)
                .Build();
    
            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("default", null)
                .StartAt(runTime)
                .WithCronSchedule("* 3 * * * ?")
                .Build();
    
            scheduler.ScheduleJob(job, trigger);
    
            this.CompletedEvent.WaitOne();
        }
    
        public override bool OnStart()
        {
            // construct a scheduler factory
            ISchedulerFactory factory = new StdSchedulerFactory();
    
            // get a scheduler
            this.scheduler = factory.GetScheduler();
            this.scheduler.Start();
    
            return base.OnStart();
        }
    
        public override void OnStop()
        {
            this.scheduler.Clear();
            this.CompletedEvent.Set();
            base.OnStop();
        }
    }
    
  10. 發行上雲端,每日執行後可以得到報表資料如下

有了每日報表之後,就算我們沒有隨時的關切線上Elmah的錯誤清單,每天也可以收到當天網站的錯誤統計資料,這有助於我們針對網站的潛在問題及風險進行管理,也可以當作網站健康度指標的依據!

延伸閱讀:

定期清除Elmah錯誤記錄

隨著時間的累積,Elmah的錯誤記錄也會隨之增加,然而由於網站持續的在改善並更新程式碼,我們不可能經常的花時間清查過久以前的Log,而為了避免錯誤資料過於龐大,我們習慣會設一個排程清除Elmah的錯誤資料,只保留比較近的日期。

  1. 在ExceptionDB新增StoredProcedure,為了避免一次刪除大量資料影響資料庫效能,我們這邊判斷過期資料後,一次只刪除小量資料

    USE [ExceptionDB]
    GO
    
    SET ANSI_NULLS ON
    GO
    
    SET QUOTED_IDENTIFIER ON
    GO
    
    CREATE PROCEDURE [dbo].[ELMAH_ClearErrorsLogs] 
    AS
    BEGIN
        SET NOCOUNT ON;
    
        DECLARE @clearDate char(8) = format(dateadd(d,-30,getdate()),'yyyyMMdd');   
    
        --Remove temp table
        IF (OBJECT_ID('tempdb..##tmpOverdateErrorId') IS NOT NULL)
            drop table #tmpOverdateErrorId;
    
        --Move overdate error id to temp table
        SELECT [ErrorId]
        into #tmpOverdateErrorId
        FROM [dbo].[ELMAH_Error]
        WHERE [TimeUtc] < @clearDate;   
    
        --Remove elmah errors
        WHILE 1 = 1 
        BEGIN
            WAITFOR DELAY '00:00:01';
    
            DELETE TOP(5) [dbo].[ELMAH_Error] 
                FROM [dbo].[ELMAH_Error] t1
                INNER JOIN #tmpOverdateErrorId t2 ON t1.[ErrorId] = t2.[ErrorId];
    
            IF @@ROWCOUNT = 0
                BREAK;
        END;
    END
    
    GO
    
  2. 更新EntityFramework的DbContext

  3. 新增Elmah_ClearErrorsLog

  4. 新增一個清除ErrorLog的工作

    public class ClearErrorLogsJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            using (ExceptionDBContext dbContext = new ExceptionDBContext())
            {
                var count = dbContext.ELMAH_ClearErrorsLogs();
    
                Trace.WriteLine("Clear error log count: " + count);
            }
        }
    }
    
  5. 在Worker Role增加一個排程

    var job = JobBuilder.Create<ClearErrorLogsJob>()
                        .WithIdentity("ClearErrorLogsJob", null)
                        .Build();
    
    ITrigger trigger = TriggerBuilder.Create()
                                     .WithIdentity("default", null)
                                     .StartAt(runTime)
                                     .WithCronSchedule("* 3 * * * ?")
                                     .Build();
    
    scheduler.ScheduleJob(job, trigger);
    
  6. 發行至雲端,設定好排程工作的執行,這麼一來我們就可以只保留需要範圍的錯誤記錄了!

本日小結

藉由排程工作的幫助,我們可以將系統的數據得到實際的報表,這麼一來就可以很方便的了解網站的實際狀況,並且安排對應的工作進行處理,也可以避免線上網站有大量的錯誤卻沒有被發現,關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (27) - 在Azure上執行排程工作

| Comments

我們在營運線上網站時,除了網站本身的維護之外,也常常會遇到需求是必須定期執行一些批次程式,有可能是用來更新靜態檔案讓資料保持在最新版本,又或是檢查系統資料是否有異常的情況發生,若有異常情況就馬上發送警告通知讓維護人員知道,而今天的內容就是要向大家介紹如何在Azure上設定排程工作並執行。

使用Windows的Task Scheduler進行排程工作

Azure的Cloud Service其實也是由一個個的VM所構成,而VM中當然會具有Windows的Task Scheduler,因此我們可以透過一些簡單的Command,讓Cloud Service在發行時新增一個排程工作,這麼一來就可以最快速的實現我們想要的排程功能,這也是所需成本最低的一種方法!

  1. 新增一個批次工作內容

    public class BatchJob
    {
        public void Execute()
        {
            MailAddress from = new MailAddress("xxx@gmail.com", "kirkchen", Encoding.UTF8);            
            MailMessage mail = new MailMessage(from, new MailAddress("xxx@gmail.com"));
    
            string subject = "Test Subject";
            mail.Subject = subject;
            mail.SubjectEncoding = Encoding.UTF8;
    
            string body = "Test Body";
            mail.Body = body;
            mail.BodyEncoding = Encoding.UTF8;
            mail.IsBodyHtml = false;
            mail.Priority = MailPriority.High;
    
            SmtpClient client = new SmtpClient();
            client.Host = "smtp.gmail.com";
            client.Port = 587;
            client.Credentials = new NetworkCredential("xxx", "xxx");
            client.EnableSsl = true;
    
            client.Send(mail);
        }
    }
    
  2. 建立成批次

    public class Program
    {
        static void Main(string[] args)
        {
            BatchJob job = new BatchJob();
            job.Execute();
    
            Console.WriteLine("Finish!");
            Console.Read();
        }
    }
    
  3. 將批次放到WebRole的專案中,並設置為複製到輸出目錄

  4. 新增cmd檔,建立排程工作

    ::Start Task Scheduler Service
    net start "task scheduler"
    
    ::Create user for schedule job
    net user jobuser1 P@ssw0rd /add
    
    ::Set user as adimn
    net localgroup Administrators jobuser1 /add
    
    ::Create Schedule job
    schtasks /create /SC MINUTE /MO 1 /TN BatchJob /TR %~dp0BatchJob/ApiSample.Consoles.Console.exe /F /RU jobuser1 /RP P@ssw0rd 
    

    註: %~dp0可以取得cmd所在目錄

  5. 修改發行專案的ServiceDefinition.csdef(或*.csdef),在發行時啟動cmd檔

    <WebRole name="ApiSample.UI.WebSite" vmsize="ExtraSmall">
      <Sites>
        <Site name="Web">
          <Bindings>
            <Binding name="Endpoint1" endpointName="Endpoint1" />
          </Bindings>
        </Site>
      </Sites>
      <Startup>
        <Task commandLine="Batchs\startuptask.cmd" executionContext="elevated" taskType="simple" />
      </Startup>
      <Endpoints>
        <InputEndpoint name="Endpoint1" protocol="http" port="80" />
      </Endpoints>
      <Imports>
        <Import moduleName="Diagnostics" />
        <Import moduleName="RemoteAccess" />
        <Import moduleName="RemoteForwarder" />
      </Imports>
    </WebRole>   
    
  6. 發行網站就完成了批次工作的建立

延伸閱讀:

使用Worker Role配合Schedule Library撰寫排程工作

除了使用VM中的Task Scheduler之外,我們也可以建立一個Worker Role,並且利用Schedule的Library執行Job的方式來執行排程工作

  1. 新增Cloud Service專案

  2. 新增一個背景工作角色

  3. 使用Nuget加入Quartz.Net Library

  4. 新增一個批次的邏輯

    public class BatchJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            Trace.Write("Execute Job");
    
            MailAddress from = new MailAddress("xxx@gmail.com", "kirkchen", Encoding.UTF8);            
            MailMessage mail = new MailMessage(from, new MailAddress("xxx@gmail.com"));
    
            string subject = "Test Subject";
            mail.Subject = subject;
            mail.SubjectEncoding = Encoding.UTF8;
    
            string body = "Test Body";
            mail.Body = body;
            mail.BodyEncoding = Encoding.UTF8;
            mail.IsBodyHtml = false;
            mail.Priority = MailPriority.High;
    
            SmtpClient client = new SmtpClient();
            client.Host = "smtp.gmail.com";
            client.Port = 587;
            client.Credentials = new NetworkCredential("xxx", "xxx");
            client.EnableSsl = true;
    
            client.Send(mail);
        }
    }
    
  5. 在Worker Role使用Quartz.Net設定批次的排程

    public class WorkerRole : RoleEntryPoint
    {
        private IScheduler scheduler;
    
        private ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
        public override void Run()
        {
            DateTimeOffset runTime = DateBuilder.EvenMinuteDate(DateTime.UtcNow);
            DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 10);
    
            var job = JobBuilder.Create<BatchJob>()
                .WithIdentity("BatchJob", null)
                .Build();
    
            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("default", null)
                .StartAt(runTime)
                // execute preiod with cron format
                .WithCronSchedule("1 * * * * ?")
                .Build();
    
            scheduler.ScheduleJob(job, trigger);
    
            this.CompletedEvent.WaitOne();
        }
    
        public override bool OnStart()
        {
            // construct a scheduler factory
            ISchedulerFactory factory = new StdSchedulerFactory();
    
            // get a scheduler
            this.scheduler = factory.GetScheduler();
            this.scheduler.Start();
    
            return base.OnStart();
        }
    
        public override void OnStop()
        {
            this.scheduler.Clear();
            this.CompletedEvent.Set();
            base.OnStop();
        }
    }
    
  6. 發行背景工作角色到雲端,完成排程批次的設定!

延伸閱讀:

使用MobileService和Service Bus Queue來執行批次工作

  1. 在Service Bus新增一個服務,並取得金鑰

  2. 建立一個行動服務,並且選擇排程器

  3. 建立一個排程工作

  4. 輸入指令碼,透過排程器,時間到時新增一個Queue Message

    function BatchJob() {
        var azure = require('azure');
        console.info('Start TestService Bus');
    
        var queueService = azure.createServiceBusService('[servicebus name]','[servicebus key');    
    
        queueService.createQueueIfNotExists('job', function(error){
            if(!error){
                console.info(error);
            }
        });
    
        var message = {
            body: 'BatchJob'
        };
    
        queueService.sendQueueMessage('job', message, function(error){
            if(!error){
                console.info(error);
            }
        });
    }
    
  5. 執行一次後,可以看到確實新增了資料到佇列中

  6. 我們只要建立一個Worker Role,在有訊息佇列時執行工作即可

    public class WorkerRole : RoleEntryPoint
    {
        // 佇列的名稱
        const string QueueName = "job";
    
        // QueueClient 可進行安全對話。已建議您進行快取, 
        // 而非在每次要求時重新建立
        QueueClient Client;
        ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
        public override void Run()
        {
            Trace.WriteLine("開始處理訊息");
    
            // 起始訊息幫浦,且已叫用每則已收到的訊息回呼,用戶端結果的呼叫會停止幫浦。
            Client.OnMessage((receivedMessage) =>
                {
                    try
                    {
                         var message = receivedMessage.GetBody<string>();
                         if(message!="BatchJob"){
                             return;
                         }
    
                         MailAddress from = new MailAddress("xxx@gmail.com", "kirkchen", Encoding.UTF8);            
                         MailMessage mail = new MailMessage(from, new MailAddress("xxx@gmail.com"));
    
                         string subject = "Test Subject";
                         mail.Subject = subject;
                         mail.SubjectEncoding = Encoding.UTF8;
    
                         string body = "Test Body";
                         mail.Body = body;
                         mail.BodyEncoding = Encoding.UTF8;
                         mail.IsBodyHtml = false;
                         mail.Priority = MailPriority.High;
    
                         SmtpClient client = new SmtpClient();
                         client.Host = "smtp.gmail.com";
                         client.Port = 587;
                         client.Credentials = new NetworkCredential("xxx", "xxx");
                         client.EnableSsl = true;
    
                         client.Send(mail);
    
                        receivedMessage.Complete();
                    }
                    catch
                    {
                        receivedMessage.Abandon();
                    }
                });
    
            CompletedEvent.WaitOne();
        }
    
        public override bool OnStart()
        {
            // 設定並行連線數目上限 
            ServicePointManager.DefaultConnectionLimit = 12;
    
            // 若不存在,請建立佇列
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
            if (!namespaceManager.QueueExists(QueueName))
            {
                namespaceManager.CreateQueue(QueueName);
            }
    
            // 起始到 Service Bus 佇列的連線
            Client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
            return base.OnStart();
        }
    
        public override void OnStop()
        {
            // 關閉到 Service Bus 佇列的連線
            Client.Close();
            CompletedEvent.Set();
            base.OnStop();
        }
    }
    
  7. 開啟MobileService排程以及發行批次執行的Worker Role即可

延伸閱讀:

本日小結

透過一些簡單的排程工作,可以減低平常我們在維運網站時的手工作業,更甚至可以藉由一些定時的資料檢查來確認系統有沒有異常的狀況,今天主要也是提供一些在Azure上設立排程工作的方法給大家,大家可以依據自己的使用情境選擇適合的來使用,關於今天的內容,歡迎大家一起討論 ^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (26) - 使用Azure Service Bus處理瞬間大量請求

| Comments

在我們網站開發時,有時候會遇到瞬間大量請求,而系統卻來不及處理,導致網頁發生無法回應的情況,例如常常出現在活動開始報名,或是開放登記新手機等等場景,問題發生的原因是有可能網頁伺服器或資料庫面對Request時,必須要花時間處理,而一次能夠處理的Request數量也有限,就會造成網頁伺服器的Request Queue越來越多沒辦法消化的情況,更甚至是資料庫發生Dead lock,那就慘不忍睹了。

某些時候這種情境我們不需要馬上對資料進行變更或是即時回應給使用者,可以考慮先將傳入的訊息存入Service Bus的佇列中,讓完成請求的時間變短,然後系統在慢慢對佇列中的內容進行消化,已確保使用者的請求有被處理到,今天我就要向大家介紹如何使用Azure Service Bus來處理訊息佇列。

使用情境說明

假設我們網站的API開放給使用者進行新手機登記活動,假設因為伺服器等級的關係,我們處理每一個使用者進行登記請求,由於還要操作資料庫,透過Third Party發送簡訊等等行為,大約要五秒鐘才能回應給使用者是否登記成功,在一般流量的情況下是沒有什麼問題的。

但如果今天是熱門手機上市了,我們同時要面對幾千人進行登記,如果一個人要5秒鐘,那第一百個人要等待500秒伺服器才會回應它的話,那麼可能使用者都要抱怨連連了。

為了改善這種很不好的使用者體驗,我們可以利用佇列的特性,因為它是First In First Out,就算進入佇列中,我們還是可以保持使用者的先後順序,先讓使用者得到登記的回應,我們在透過後段的程式慢慢處理登記的流程。

這樣的模式讓使用者進行登記,而事後再接受到登記確認,使用者不會因為沒辦法送出Request而抱怨,而後續的動作就算需花比較久的時間,但也能夠確實的完成了!

建立Azure Service Bus服務

我們首先先在Azure建立一個Service Bus服務,用來儲存請求資料佇列

  1. 進入Azure入口,選擇建立命名空間

  2. 加入新的命名空間

  3. 建立完成後,可以看到下方有連接資訊,記住連接字串

撰寫登記API

接下來我們要來修改我們的API,提供使用者登記新產品,並儲存到Azure Service Bus的Queue中

  1. 在ViewModels新增登記資料的Class

    public class RegisterProductUserInfoModel
    {        
        public string Name { get; set; }
    
        public string CellPhone { get; set; }
    
        public string Email { get; set; }
    }    
    
  2. 在網站的ProductController新增登記商品Action

    public class ProductController : JsonNetController
    {            
        public IProductService ProductService { get; set; }
    
        public ILogger Logger { get; set; }
    
        public ProductController(IProductService productService, ILogger logger)
        {
            this.ProductService = productService;
            this.Logger = logger;
        }            
    
        [AuthorizeByToken(Roles = "Administrator")]
        public ActionResult RegisterNewProduct(RegisterProductUserInfoModel model)
        {
            this.ProductService.RegisterNewProduct(model);
    
            return Json(ApiStatusEnum.Success);
        }
    }
    
  3. 使用Nuget在BL中安裝Azure Service Bus的Library

  4. 在BL將資料送到RegisterProduct的佇列中

    public interface IProductService
    {            
        void RegisterNewProduct(RegisterProductUserInfoModel model);
    } 
    
    public class ProductService : IProductService
    {            
        public void RegisterNewProduct(RegisterProductUserInfoModel model)
        {            
            string connectionString = CloudConfigurationManager.GetSetting("azure.servicebus.connectionstring");
    
            // Create the queue if it does not exist already
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);            
            if (!namespaceManager.QueueExists("RegisterProduct"))
            {
                namespaceManager.CreateQueue("RegisterProduct");
            }                
    
            QueueClient client = QueueClient.CreateFromConnectionString(connectionString, "RegisterProduct");
            client.Send(new BrokeredMessage(model));            
        }
    }
    
  5. 嘗試送出資料給API

  6. 送出成功後可以到Azure看到訊息有確實增加

撰寫佇列處理處理程式

在我們可以使用佇列來先儲存使用者送進來的登記需求之後,我們還需要一個程式來幫我們處理佇列中的需求,讓使用者的資料可以確實寫入資料庫,並發送登記成功的訊息

  1. 我們新增一個Cloud專案

  2. 選擇Service Bus的Worker role,會建立一個Cloud專案,和一個類別庫專案

  3. 其實到這邊大部分的程式碼都已經建立好了,我們先點擊Cloud專案角色,輸入Service Bus的連線字串

  4. 在類別庫專案加入我們需要的程式碼邏輯

    public class WorkerRole : RoleEntryPoint
    {
        // 佇列的名稱
        const string QueueName = "RegisterProduct";
    
        // QueueClient 可進行安全對話。已建議您進行快取, 
        // 而非在每次要求時重新建立
        QueueClient Client;
        ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
        public override void Run()
        {
            Trace.WriteLine("開始處理訊息");
    
            // 起始訊息幫浦,且已叫用每則已收到的訊息回呼,用戶端結果的呼叫會停止幫浦。
            Client.OnMessage((receivedMessage) =>
                {
                    try
                    {
                        var model = receivedMessage.GetBody<RegisterProductUserInfoModel>();
    
                        // 寫入資料庫
                        Trace.WriteLine("寫入資料庫...");
                        Trace.WriteLine("姓名: "+model.Name);
                        Trace.WriteLine("Email: " + model.Email);
    
                        // 發送訊息
                        Trace.WriteLine("發送訊息...");
                        Trace.WriteLine("發送簡訊給:" + model.CellPhone);
    
                        receivedMessage.Complete();
                    }
                    catch
                    {
                        receivedMessage.Abandon();
                    }
                });
    
            CompletedEvent.WaitOne();
        }
    
        public override bool OnStart()
        {
            // 設定並行連線數目上限 
            ServicePointManager.DefaultConnectionLimit = 12;
    
            // 若不存在,請建立佇列
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
            if (!namespaceManager.QueueExists(QueueName))
            {
                namespaceManager.CreateQueue(QueueName);
            }
    
            // 起始到 Service Bus 佇列的連線
            Client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
            return base.OnStart();
        }
    
        public override void OnStop()
        {
            // 關閉到 Service Bus 佇列的連線
            Client.Close();
            CompletedEvent.Set();
            base.OnStop();
        }
    }
    
  5. 執行程式之後,透過模擬器可以看到有確實處理訊息

本日小結

Service Bus適用於很多可允許非同步處理的場景,我們可以使用Queue來面對大量流量的部分,再透過Daemon來慢慢消化Queue中的資料,還可以依據伺服器的乘載量來調整Daemon的實體,讓我們提供給使用者較佳的使用者體驗,也是讓我們伺服器面對大流量時多了一層防火牆,避免瞬間的衝擊。關於今天的內容歡迎大家一起討論喔^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (25) - 使用Azure Storage儲存圖片

| Comments

在以往的網頁開發中,實際檔案的儲存一直是很麻煩的課題,有可能會受到硬碟空間大小的限制,或是不同台機器要共用Filer,還有如何確保檔案不會遺失等等,有許許多多的眉眉角角,也是無形的一種管理成本開銷,而Azure提供了專門的Storage服務,甚至還提供地理備援機制,讓我們檔案服務的可靠度大大提升,今天就要來像大家介紹如何在API中實現簡單的圖片上傳機制。

開啟Azure Storage服務

首先我們先來建立一個儲存體用來存放圖片

  1. 進入Azure入口網站,點擊儲存體,選擇新增

  2. 輸入儲存體名稱並新增

  3. 新增成功

新增商品圖片修改功能

在提供上傳圖片的服務之前,我們先在商品新增一個欄位來儲存圖片網址,並且提供一個方法讓使用者可以修改它的商品圖片。

  1. 修改Product Class,新增圖片欄位

    public class Product : EntityBase
    {            
        [StringLength(300)]
        public string ImagePath { get; set; }            
    }    
    
  2. 在套建管理員新增Migration和更新資料庫

    Add-Migration AddProductImagePath
    
    Update-Database
    
  3. 在DA建立修改商品圖片的程式碼

    public interface IProductRepository
    {
        IEnumerable<ProductForCategoryModel> GetProductByCategoryId(int categoryId);
    
        void InsertProduct(ProductModel productModel);
    
        void UpdateProductImagePath(int id, string imagePath);
    }
    
    public class ProductRepository : IProductRepository
    {
        public ShopContext ShopContext { get; set; }
    
        public ProductRepository(ShopContext context)
        {
            this.ShopContext = context;
        }
    
        public void UpdateProductImagePath(int id, string imagePath)
        {
            var product = this.ShopContext.Products
                                          .Where(i => i.Id == id)
                                          .FirstOrDefault();
    
            if (product != null)
            {
                product.ImagePath = imagePath;
            }
    
            this.ShopContext.SaveChanges();
        }
    }        
    

新增Azure檔案上傳服務

  1. 新增IFileRepository介面,開放兩個參數作為分類依據

    網址格式: http://hostname/Container}/Id}/FileName}{

    public interface IFileRepository
    {
        string UploadFile(string containerName, string groupName, HttpPostedFileBase file);
    }
    
  2. 使用Nuget加入Azure.Storage Library

  3. 到Azure儲存體點選管理儲存金鑰

  4. 記下主要金鑰內容

  5. 在網站的AppSetting新增Storage的連線字串,包含主要金鑰

    <add key="azure.blob.connectionstring" value="DefaultEndpointsProtocol=https;AccountName=apiimage;AccountKey=xxxx;BlobEndpoint=http://apiimage.blob.core.windows.net/" />
    
  6. 撰寫上傳檔案至Azure Storage的程式碼

    public class FileRepository : IFileRepository
    {
        public string UploadFile(string containerName, string groupName, HttpPostedFileBase file)
        {
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("azure.blob.connectionstring"));
    
            // Create the blob client.
            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
    
            // Retrieve a reference to a container. 
            CloudBlobContainer container = blobClient.GetContainerReference(containerName.ToLower());
    
            // Create the container if it doesn't already exist.
            container.CreateIfNotExists();
    
            container.SetPermissions(
                     new BlobContainerPermissions
                     {
                         PublicAccess =
                             BlobContainerPublicAccessType.Blob
                     });
    
            var fileName = groupName + "/" + file.FileName;
            CloudBlockBlob blockBlob = container.GetBlockBlobReference(fileName);
    
            blockBlob.Properties.ContentType = file.ContentType;
            blockBlob.UploadFromStream(file.InputStream);
    
            return blockBlob.Uri.AbsoluteUri;
        }
    }
    

延伸閱讀:

測試圖片上傳

接下來我們模擬上傳圖片,測試是否有上傳到Azure Storage和更新商品圖片

  1. 使用Fiddle執行Post檔案

  2. 收到成功訊息

  3. 進入Storage的容器Product中,有圖片存在

  4. 商品資料也有確實更新

本日小結

透過Azure Storage服務,可以非常快速的就實現檔案上傳,也可以讓我們省去許多管理儲存空間的麻煩,例如像File Server的維護,還能提供高可用性的儲存空間,甚至是異地備援等等,而在使用上也擁有非常多的彈性,更不需要擔心硬碟空間不足的問題,大家可以依照自己的需求評估使用,關於今天的內容歡迎大家一起討論 ^_^

Comments

comments powered by Disqus

使用Asp.Net MVC打造Web Api (24) - 使用Azure Cache將資料進行Cache

| Comments

在大型網站應用上,Cache的使用絕對是讓人又愛又恨,若是用的好,可以讓網站的Performance大大提升,但若不謹慎使用Cache的話,最後可能會發現自己的網站怎麼更新都是舊資料。因此Cache的使用絕對必須審慎拿捏,用在關鍵點,而且一但用了Cache一定要有對應的資料更新機制,這樣才能讓Cache發揮最大的功效。

新增Cache Role

在之前的文章中,我們已經新增了Web Role用來發行網站,而今天我們要在同樣的專案中新增一個Cache Role用來提供我們的Cache服務。

  1. 在Deploy專案的角色點擊滑鼠右鍵,新增背景工作角色專案

  2. 選擇新增快取角色服務

  3. 將網站發行至雲端,就可以看到快取服務的實體已經正常運作中,進入設定頁面可以看到有基本的設定內容

替網站加入Cache

接下來我們將實作一個模擬長時間執行的資料查詢,並且替這個服務加上Cache

  1. 使用Nuget加入Azure cache的Library

  2. 修改config,將[cache cluster role name]改為我們剛剛新增的Role名稱

    <dataCacheClients>
      <dataCacheClient name="default">
        <!--To use the in-role flavor of Windows Azure Caching, set identifier to be the cache cluster role name -->
        <!--To use the Windows Azure Caching Service, set identifier to be the endpoint of the cache cluster -->
        <autoDiscover isEnabled="true" identifier="ApiSample.Cache" />
    
        <!--<localCache isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />-->
    
        <!--Use this section to specify security settings for connecting to your cache. This section is not required if your cache is hosted on a role that is a part of your cloud service. -->
        <!--<securityProperties mode="Message" sslEnabled="false">
          <messageSecurity authorizationInfo="[Authentication Key]" />
        </securityProperties>-->
      </dataCacheClient>
    </dataCacheClients>    
    
  3. 撰寫一個模擬長時間查詢的服務

    public interface ILongTimeService
    {
        string GetLongTimeData();
    }
    
    public class LongTimeService : ILongTimeService
    {
        public string GetLongTimeData()
        {
            System.Threading.Thread.Sleep(10 * 1000);
    
            return DateTime.Now.ToString();
        }
    }
    
  4. 增加一個Controller提供資料,並且加上Cache,同時我們加上StopWatch觀察查詢資料需要的時間

    public class SampleController : Controller
    {            
        public SampleController(ISampleService sampleService, ILongTimeService longTimeService)
        {
            this.SampleService = sampleService;
            this.LongTimeService = longTimeService;
        }  
    
        public ActionResult GetLongTimeData()
        {
            //// Calculate execute time
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();
    
            //// Emulate long time processing data
            DataCacheFactory cacheFactory = new DataCacheFactory();
            DataCache cache = cacheFactory.GetDefaultCache();
            object result = cache.Get("longTimeData");
            if (result == null)
            {
                result = this.LongTimeService.GetLongTimeData();
                cache.Add("longTimeData", result);
            }
    
            stopWatch.Stop();
            TimeSpan ts = stopWatch.Elapsed;
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
                ts.Hours, ts.Minutes, ts.Seconds,
                ts.Milliseconds / 10);
    
            return Json(
                new
                {
                    Result = result,
                    ExecuteTime = elapsedTime
                }, JsonRequestBehavior.AllowGet);
        }
    
    }
    

    其實cacheFactory.GetDefaultCache();代表取得default這組cache,也可以使用cacheFactory.GetCache("cache name");取得其它Cache資料。

  5. 部署程式到雲端,準備開始測試,由於是長時間查詢,所以第一次的執行時間應該會比較久,我們連結至/Sample/GetLongTimeData測試

  6. 再次執行查詢,由於第一次查詢之後結果應該會儲存在Cache之中,所以查詢時間應該較短!

測試成功!我們的Cache服務生效了,大家還可以依據自己的需求進一步的調整Cache的時間長短。

延伸閱讀:

使用AOP實作CacheInterceptor

除了直接在程式碼直接撰寫Cache的讀取程式碼之外,其實我們也可以將Cache抽出作為Interceptor,這麼一來我們只要在需要的Class上加上Attribute就可以完成Cache的新增,並且讓Cache統一並分離於商業邏輯之外,也增加了重複使用的容易性!

  1. 在Extensions專案新增CacheInterceptor

    public class CacheInterceptor : IInterceptor
    {
        private string section;
    
        public CacheInterceptor()
            : this(string.Empty)
        {
        }
    
        public CacheInterceptor(string section)
        {
            this.section = section;
        }
    
        public void Intercept(IInvocation invocation)
        {
            DataCacheFactory cacheFactory = new DataCacheFactory();
    
            //// Get cache by section
            DataCache cache;
            if (string.IsNullOrWhiteSpace(this.section))
            {
                cache = cacheFactory.GetDefaultCache();
            }
            else
            {
                cache = cacheFactory.GetCache(this.section);
            }
    
            //// Get cache or set by proceed method
            var typeName = invocation.TargetType.FullName;
            var methodName = invocation.Method.Name;
            var cacheKey = string.Format("{0}-{1}", typeName, methodName);
            var result = cache.Get(cacheKey);
            if (result == null)
            {
                invocation.Proceed();
                result = invocation.ReturnValue;
    
                cache.Add(cacheKey, result);
            }
            else
            {
                invocation.ReturnValue = result;
            }
        }
    }
    
  2. 修改BL的ServiceModule,讓LongTimeService使用CacheInterceptor

    public class ServiceModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var service = Assembly.Load("ApiSample.BL.Services");
    
            builder.RegisterAssemblyTypes(service)
                   .AsImplementedInterfaces()
                   .EnableInterfaceInterceptors();
    
            builder.RegisterType<ProductService>()
                   .As<IProductService>()
                   .EnableInterfaceInterceptors()
                   .InterceptedBy(typeof(LogInterceptor), typeof(AuthInterceptor));
    
            builder.RegisterType<LongTimeService>()
                   .As<ILongTimeService>()
                   .EnableInterfaceInterceptors()
                   .InterceptedBy(typeof(CacheInterceptor));
        }
    }
    
  3. 修改Controller,將原本手動新增的Cache程式碼移除

    public ActionResult GetLongTimeData()
    {
        //// Calculate execute time
        Stopwatch stopWatch = new Stopwatch();
        stopWatch.Start();
    
        var result = this.LongTimeService.GetLongTimeData();
    
        stopWatch.Stop();
        TimeSpan ts = stopWatch.Elapsed;
        string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
            ts.Hours, ts.Minutes, ts.Seconds,
            ts.Milliseconds / 10);
    
        return Json(
            new
            {
                Result = result,
                ExecuteTime = elapsedTime
            }, JsonRequestBehavior.AllowGet);
    }
    
  4. 發行上雲端,測試一切正常,如此一來不但讓我們的程式碼更加的乾淨,也讓要增加Cache變成一件輕鬆的事情囉!

本日小結

透過Cache的幫助,我們可以讓需要花費大量資源才能產生資料的服務成本降低,也讓網站整體的效能變好,但還是必須謹慎的使用以避免Cache過於繁雜而難於管理,Azure提供的分散式Cache服務也讓Cloud Service可以共用Cache服務,更甚至透過多個Cache Instance來實現高可用性,相當的方便,大家可以多加利用!關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus