Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 05] 打造第一個通用服務 - Log

到目前為止,應該對於Autofac的使用有了基本的了解。在上一篇用了一個簡單的Log服務來說明Autofac如何和Mvc結合。

Log屬於任何一個系統必須有的服務,因此在這一篇,我們打造真的Log服務。

建立的流程

基本上會和我們在上一篇打造那個服務一樣,我們會:

1VZPk6I+EP00OY4lf1Q8AuL+Lnvy8DtHaCA1gVAhKM6nn442IuPurFM7tVNzschL0+l+73WQeXHV/9C8KX+qDCRz51nPvA1z3cBf4a8FThfAXc8vQKFFdoGcEdiJFyBwCOtEBu0k0CgljWimYKrqGlIzwXIlp0c0vBjSj8Au5fIe/V9kpqQe3OWI/weiKIdjnOX6stOa05Ajg5x30jydIdyz2xUfclFX/ZzY8CnBiQCPAhpeT0p6UaqaABrakStqV1BdlGOvdAZ6AklRP9/y5iUonVYKX7RPVR+DtPIN0viZk+2xy0W6z/21s3q65Nk+Gn4lVENNxf1tSqL0wGVH7bNkwaKIRSFLViyMWBCJ2oDOeQrMSuf1nHkRS3wWbdg6tlGBi+FhZxQG2fU6ZIG/A30Q9p03irZGq+erIZC0qC15YzervrCmn+VSHdOSazNrtEqhRYqjXNWG7Oz4VDNoAzQYjzAzSoTTBaoCo0/WPOSdBYlKk+VRyuPoXHdB9ipvXDtgnIxQXDOPMuADKfGgKt4vVQm3LFxeiR9VGRiPJUeuPqJRrKpG1Zayb6qS489niy8UamjyRqi3TEKdhVqrI65SK5BILZkGiRvgGiUYsK2wh2/w/orwRVoRb5Dd3bd/IhIrUZ1Gj7w7/XhwAb/Lc/bivSAaJDfiMC3oU7klXSdDgL71WRjE6DMuatBRJ6S9lZMAUXtt4ZgESxaE39XQ7hcbmr7NHzb0lKh/Yl66JN8175nKzzcvLsfP/Hnv5u+al7wC
建立Service和Component的流程
  1. 定義Log所會擁有的Service(interface)
  2. 實作一個我們框架會用的Log Component
  3. 註冊到ContainerBuilder

定義Log擁有的Service

基本上,一般的log都有一些log層級,可以讓我們寫log的時候區分那些屬於錯誤(Error),和那些是偵錯(Debug)時候看的。因此,有以下幾個層級:

  1. Trace
  2. Debug
  3. Info
  4. Warn
  5. Error
  6. Fatal

層級定義完成之後,我們要決定每一個層級要有那些寫log的方法:

  1. (string message) - 只是把message輸出
  2. (object outpuObject) - 把物件資訊印出來
  3. (string message, params object[] args) - message是訊息,可以用string format一樣的placeholder,而args是placeholder的值
  4. (string message, object outputObject) - 要輸出的訊息和把物件資訊印出來
當然,上面這些定義是符合我自己使用,而每一個情況不一樣,因此各位應該客制屬於自己需要的方法。

最後,由於有6個log層級和每一個層級都有4個方法,總共有24個需要定義,這邊我只定義某一個層級的4個方法,剩下都會一樣:

/// <summary>
/// Log功能的interface
/// </summary>
public interface ILog
{
    /// <summary>
    /// Traces 訊息
    /// </summary>
    /// <param name="message">訊息</param>
    void Trace(string message);

    /// <summary>
    /// Traces 把某個物件內容dump出來
    /// </summary>
    /// <param name="outputObject">要dump的物件</param>
    void Trace(object outputObject);

    /// <summary>
    /// Traces 訊息加上format的參數
    /// </summary>
    /// <param name="message">訊息</param>
    /// <param name="args">format的參數</param>
    void Trace(string message, params object[] args);

    /// <summary>
    /// Traces 把某個物件內容dump出來,並且在dump內容加上一段訊息
    /// </summary>
    /// <param name="message">加上的訊息</param>
    /// <param name="outputObject">要dump的物件</param>
    void Trace(string message, object outputObject);

// 。。。。 其他log層級的方法定義

定義ILog的實作


有了Service定義好了之後,我們就要來決定我們要如何實作ILog。


第一件事情是決定自己要使用的Log Framework。比較出名的有NLog和Log4Net。我這邊會選擇使用NLog。



NLog



設定一些注入用的參數


有用過Log Framework就會知道,通常我們會想要知道,是哪一個Class寫出了某一筆的log,因此在建立Log的class的時候能夠傳入要用這個log的Class 名稱。


我們也希望我們的Log Framework有這個功能,因此我們需要先定義出來,好讓Autofac能夠幫忙注入Class名稱。

/// <summary>
/// 使用Nlog作為ILog的實作
/// </summary>
public class NlogLogger : ILog
{
    // NLog 物件
    private Logger logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="NlogLogger"/> class.
    /// </summary>
    public NlogLogger()
    {
        logger = NLog.LogManager.GetCurrentClassLogger();
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="NlogLogger"/> class.
    /// </summary>
    /// <param name="name">目前要使用Log的Class名字</param>
    public NlogLogger(string name)
    {
        logger = NLog.LogManager.GetLogger(name);
    }
	
// ..其他實作

我們透過的方式會是用Constructor來傳遞我們class的名稱。


定義interface的實作


再來我們要實際實作interface定義的24個方法。


有兩個部分需要特別處理:



  1. 怎麼把物件資訊dump出來
  2. log framework通常都能夠判斷某個層級是否需要開放輸出


針對第一個,我將會使用Json.Net來把物件資訊dump出來。這個有好有懷,好處是不用寫複雜的邏輯來把物件印出來,而且顯示的樣式是熟悉的Json格式。而壞處是我們多了一個json .Net的 dependency。



Json .Net


屬於必裝型的套件。主要工作室Class <=> Json之間互相的轉換。效能上面比內建的快(根據他們自己的評測)



針對第二個,我們每個層級的四個方法都用統一的一個方法做輸出,這樣就會做判斷需不需要實際呼叫:

// 還是在NlogLogger.cs 裡面

/// <summary>
/// Traces 訊息
/// </summary>
/// <param name="message">訊息</param>
public void Trace(string message)
{
    if (logger.IsTraceEnabled)
    {
        logger.Trace(message);
    }
}

  /// <summary>
/// Traces 訊息加上format的參數
/// </summary>
/// <param name="message">訊息</param>
/// <param name="args">format的參數</param>
public void Trace(string message, params object[] args)
{
    Trace(string.Format(message, args));
}

  /// <summary>
/// Traces 把某個物件內容dump出來
/// </summary>
/// <param name="outputObject">要dump的物件</param>
public void Trace(object outputObject)
{
    Trace(JsonConvert.SerializeObject(outputObject, Formatting.Indented));
}

/// <summary>
/// Traces 把某個物件內容dump出來,並且在dump內容加上一段訊息
/// </summary>
/// <param name="message">加上的訊息</param>
/// <param name="outputObject">要dump的物件</param>
public void Trace(string message, object outputObject)
{
    Trace(message + Environment.NewLine +
	 	JsonConvert.SerializeObject(outputObject, Formatting.Indented));
}

// ..其他層級實作如上


上面顯示了Trace層級的方法實際定義,其他幾個層級的實作會一樣。


在ContainerBuilder註冊


在這邊有一點是之前沒有講過的,我們的NlogLogger裡面是沒有無參數的建構子。而我們接受的參數只是一個string的參數叫做name。那麼,照著我們目前所了解的註冊裡面,是沒有辦法解決這個問題。不過,Autofac當然有想到這種情況,因此我們這邊乘著這個機會介紹一下。


我們先了解一下,string name的參數是要傳入什麼?


我們之前講過,log framework都有一個參數是記錄寫這個log的class 名稱。因此我們這個參數代表就是這個要用NlogLogger的class 名稱。


那要如何注入這個class名稱呢?在Autofac裡面有Module可以讓我們設定特殊的註冊邏輯。同時,在Module裡面有提供event讓我們可以再Autofac實例化component的時候,做一些事情。因此我們可以透過這個方法來注入我們class的名稱。



建立NlogModule


建立一個Autofac的Module需要建立一個Class繼承Autofac.Module


我們這邊會複寫兩個method:



  1. Load - Module註冊第一個會執行的方法。這邊可以設定我們的NlogLogger將會作為ILog service的Component。
  2. AttachToComponentRegistration這邊就是讓我們可以註冊一些在建立時候的事件



這一段的程式碼是參考網路上面的資料,因此有些註解是英文。

// NLogModule.cs
 
/// <summary>
/// Dependency Injection Module 用來註冊ILog將會使用NLog
/// </summary>
public class NLogModule : Autofac.Module
{
	/// <summary>
    /// Override to add registrations to the container.
    /// </summary>
    /// <param name="builder">The builder through which components can be
    /// registered.</param>
    /// <remarks>
    /// Note that the ContainerBuilder parameter is unique to this module.
    /// </remarks>
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<NlogLogger>()
          .As<ILog>();
    }

    /// <summary>
    /// 增加透過用Constructor或者Property的方式注入
    ///		<see cref="MvcInfrastructure.Common.Log.NlogLogger"/>
    ///		為<see cref="MvcInfrastructure.Common.Log.ILog"/>的實作
    /// </summary>
    /// <param name="componentRegistry">The component registry.</param>
    /// <param name="registration">The registration to attach functionality to.</param>
    /// <remarks>
    /// This method will be called for all existing <i>and future</i> component
    /// registrations - ordering is not important.
    /// </remarks>
    protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, 
			IComponentRegistration registration)
    {
        // Handle constructor parameters. 處理Constructor注入
        registration.Preparing += OnComponentPreparing;
        
        // Handle properties. 處理Property注入
        registration.Activated += (sender, e) => 
			InjectLoggerProperties(e.Instance);
    }
}

從上面可以看到,在AttachToComponentRegistration我們註冊了兩個事件,一個是用來處理Constructor的時候注入參數,另外一個是用Property的方式注入。



我們實作的版本是沒有允許用Property的方式注入,不過保留這個方法僅供參考。


接下來我們看一下,Constructor注入的方法是如何寫的:

//NLogModule.cs

// ....
/// <summary>
/// Called when [component preparing]. 用來增加Constructor方式注入
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="PreparingEventArgs"/> instance containing the event data.</param>
private static void OnComponentPreparing(object sender, PreparingEventArgs e)
{
    var t = e.Component.Activator.LimitType;
    e.Parameters = e.Parameters.Union(
        new[]
        {
            new ResolvedParameter((p, i) => p.ParameterType == typeof(ILog),
                (p, i) => i.Resolve<ILog>(new NamedParameter("name", t.FullName)))
        });
}

// .....

最後,看一下如果用Property Inject的話是如何實現

//NLogModule.cs

// ....

/// <summary>
/// Property注入的邏輯
/// </summary>
/// <param name="instance">目前被實例化的Class Instance</param>
private static void InjectLoggerProperties(object instance)
{
    var instanceType = instance.GetType();

    // Get all the injectable properties to set.
    // If you wanted to ensure the properties were only UNSET properties,
    // here's where you'd do it.
    var properties = instanceType
      .GetProperties(BindingFlags.Public | BindingFlags.Instance)
      .Where(p => p.PropertyType == typeof(ILog) && p.CanWrite && p.GetIndexParameters().Length == 0);

    // Set the properties located.
    foreach (var propToSet in properties)
    {
        propToSet.SetValue(instance, new NlogLogger(instanceType.FullName), null);
    }
}

// .....

到這邊,我們的Autofac.Module就建立好了。


註冊Autofac.Module


之前註冊的時候也沒有提到如何註冊Autofac.Module,這一次一起介紹。


其他註冊的部分我們就不看了,就只看註冊Module的部分:

//Global.asax

// ....

Builder.RegisterModule<NLogModule>();

// .....

這樣註冊就完成了,至於使用,就和上一篇介紹那樣,在要用到ILog的Controller裡面,把Constructor有個參數接受ILog形態的參數即可。


結語


透過這一篇,我們就知道了如何建立一個Log的功能,並且如何作為我們第一個框架的服務。


在下一篇開始,將會介紹再用Mvc開發的時候,最常用到的ViewModel概念和為什麼要使用ViewModel。


有關於程式碼的部分,稍後會補上整個專案,以供參考。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus