前言

Sinyi.Net協助開發人員快速建立Web站台、API服務和Console程式。提供參數管理、依賴注入、身分驗證等多種功能模組,用以簡化開發流程並滿足多變的商業需求。

架構流程

Sinyi.Net架構圖.drawio (2)

Sinyi.Net架構主要分為

  • Presentation(表現層): 服務對外的介面,常見的API介面或是Web畫面呈現跟背景程式等等.

  • Domain(邏輯層): 負責整個服務的核心邏輯,以架構預設為

    • Context: 依需求去取資料並組合與判斷邏輯的核心區塊.(必要)
    • Entity: 針對服務的需求或特性去定義的物件實體,它有獨特識別符號,如ID,並且具有相應行為和狀態。實體是邏輯層的核心,用於表達領域中的重要概念和業務規則。(必要)
    • Repository: 將實體Entity的持久化操作抽象化,提供邏輯層資料層之間的交互接口.
    • Provider: 負責將第三方的資料以實體去設計並提供給其他區塊.
    • Service: 若邏輯符合一個情境可設計成一組Service讓邏輯更簡化.

    除了ContextEntity之外其他都是針對使用情境與條件做設計.

  • Access(資料存取層): 串接資料的階層,依據資料來源渠道進行命名.

外界透過表現層的接口執行動作,在邏輯層依據執行條件來匹配符合的邏輯。而在依據情境去使用對應的實體並產生行為,再透過行為去資料層進行所需的資料串接。


底層套件

Sinyi.Net架構下運作是依靠兩個底層套件做運行,一個是放置於起始檔的Sinyi.AspNetCore/Sinyi.NetCore跟放置在需依賴注入物件的其他專案檔Sinyi.Registration

Sinyi.Net架構圖.drawio (3)

  • Sinyi.AspNetCore/Sinyi.NetCore: 置於專案起始檔內,是Sinyi.Net的核心。負責專案啟動、依賴注入調用與其她底層功能封裝。
  • Sinyi.Registration: 負責依賴注入的註冊,置於須手動進行依賴注入的專案檔。如何透過此套件進行注入後面會有說明。

依賴注入

依賴注入(DI)是一種設計模式,透過外部化依賴項目的方式,使程式碼更靈活、易測試、易維護。透過將依賴項目注入到類別中,提高了程式碼的鬆耦合性、可測試性、可維護性、靈活性和可擴展性。

而Sinyi.Net包裝並調整了原生.NetCore的依賴注入方式,透過設定檔的階層設計去進行註冊與調用。讓開發者專注於邏輯開發且透過靈活的注入方式為不同情境做架構設計。

基本情境

我們先預設我們有一個負責建築服務的核心邏輯取名為BuildingContext,其方法GetHouse需使用為IHouseRepository的房子介面的實例MsSqlHouseRepository。可以得知其實例是從MsSql取得房子資訊。

關係如下圖:
image

在原生.NetCore會是在起始檔寫依賴注入的語法進行註冊。

using Sinyi.building;
using Sinyi.building.Accesses;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddTransient<BuildingContext>();
builder.Services.AddTransient< MsSqlHouseRepository>();

BuildingContext在建構子注入IhouseRepository的實例。

namespace Sinyi.building
{
    public class BuildingContext
    {
        // Fields
        private readonly IHouseRepository _houseRepository;

        // Constructors
        public BuildingContext(IHouseRepository houseRepository)
        {
            _houseRepository = houseRepository;
        }

        // Methods
        public House GetHouse()
        {
            var house = this._houseRepository.GetHouse();

            return house;
        }
    }
}

而在Sinyi.Net的依賴注入模式,底層會依據設定檔的階層進行解析並透過註冊的實例進行比對再做注入動作,以下為在Sinyi.Net下的依賴注入方法。

先依據config與對應環境在Sinyi.XXX.json內作階層組合。
image

以BuildingContext為例其在建構子注入參數中有個命名為houseRepository的實例,故在以BuildingContext為命名的階層內新增個key名為HouseRepository(大小寫無區別)且value設為要使用實例的型態。

而所需使用的實例也必須註冊,故需以其型態為命名組合它的階層。

{
    //請以專案名稱命名,底層會
  "Sinyi.building": {

    // BuildingContext
    "BuildingContext": {
      //key為HouseRepository對應建構子內參數的名稱(不分大小寫),value為對應的實例type
      "HouseRepository": "MsSqlHouseRepository"
    },

    // IHouseRepository
    "MsSqlHouseRepository": {}
  }
}

而在需註冊注入的實例需添加RegisterService屬性。

namespace Sinyi.building
{
    //需添加RegisterService讓底層進行此類別得依賴注入註冊
    [RegisterService<BuildingContext>()]
    public class BuildingContext
    {
        // Fields
        private readonly IHouseRepository _houseRepository;

        // Constructors
        public BuildingContext(IHouseRepository houseRepository)
        {
            // Default
            _houseRepository = houseRepository;
        }

        // Methods
        public House GetHouse()
        {
            var house = _houseRepository.GetHouse();

            // Return
            return house;
        }
    }
}

若實例以intetface進行註冊與使用,則需帶入interface的型態。

using Sinyi.Registration;

namespace Sinyi.building.Accesses
{
    //需添加RegisterService讓底層進行此類別得依賴注入註冊
    [RegisterService<IHouseRepository>()]
    public class MsSqlHouseRepository : IHouseRepository
    {
        // Constructors
        public MsSqlHouseRepository() { }

        // Methods
        public House GetHouse()
        {
            //sql command

            var house = new House() { Name = "Sql House" };
            return house;
        }
    }
}

不同資料來源但同類型的情境

假設我們的房子資訊除了從我們自己資料庫獲得之外,也需透過外面第三方的資料來源來匹配或更新邏輯。勢必須開一條渠道在BuildingContext做整合。

假設以從google的API獲取房子資訊,關係圖如下:
image

其情境在在原生.NetCore的依賴注入語法稍有修改:

using Sinyi.building;
using Sinyi.building.Accesses;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddTransient<MsSqlHouseRepository>();
builder.Services.AddTransient<FromGoogleHouseRepository>();

//把剛加的DI Services取出來
var serviceProvider = builder.Services.BuildServiceProvider();
var sqlHouse = serviceProvider.GetService<MsSqlHouseRepository>();
var googleHouse = serviceProvider.GetService<FromGoogleHouseRepository>();

//依據參數對應去注入符合情境的BuildingContext實例
builder.Services.AddTransient(sp =>
{
    return new BuildingContext(sqlHouse, googleHouse);
});

其在BuildingContext的調整:

namespace Sinyi.building { public class BuildingContext { // Fields private readonly IHouseRepository _sqlHouseRepository; private readonly IHouseRepository _fromGoogleRepositroy; // Constructors public BuildingContext(IHouseRepository sqlHouseRepository, IHouseRepository fromGoogleRepositroy) { _sqlHouseRepository = sqlHouseRepository; _fromGoogleRepositroy = fromGoogleRepositroy; } // Methods public IList<House> GetHouse() { List<House> houselist = new(); var sqlHouse = _sqlHouseRepository.GetHouse(); houselist.Add(sqlHouse); var googleHouse = _fromGoogleRepositroy.GetHouse(); houselist.Add(googleHouse); return houselist; } } }

雖然符合需求情境,但若專案隨著推行不斷增加須注入的模組或不調整程式,像是因環境不同而有不同的注入條件或是注入模組過多階層過於複雜將會導致開發者維護困難。

在Sinyi.Net的架構下,只需為須註冊的模組增添RegisterService屬性。

using Sinyi.Registration;

namespace Sinyi.building.Accesses
{
    [RegisterService<IHouseRepository>()]
    public class FromGoogleHouseRepository : IHouseRepository
    {
        public FromGoogleHouseRepository(){ }

        public House GetHouse()
        {
            var house = new House() { Name = "Google House"};
            return house;
        }
    }
}

在以上面BuildingContext的建構子參數為例,在對應環境的設定檔做階層命名:

{
  "Sinyi.building": {

    // BuildingContext
    "BuildingContext": {
      //key為BuildingContext參數的名稱,於value為下面註冊的實例名稱(型態名稱)
      "sqlhouseRepository": "MsSqlHouseRepository",
      "fromGoogleRepository": "FromGoogleHouseRepository"
    },

    // IHouseRepository
    "MsSqlHouseRepository": {},
    "FromGoogleHouseRepository": {}
  }

環境不同的情境

若情境會需要根據不同環境有不同的注入實例,像是僅測試區不能使用到資料庫與第三方資料,須自產假資料作提供跑完服務。那在原生的案例會需要在專案起始檔的部分針對注入做一些條件判斷。

///get Environment
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddTransient<MsSqlHouseRepository>();
builder.Services.AddTransient<FromGoogleHouseRepository>();

//把剛加的DI Services取出來
var serviceProvider = builder.Services.BuildServiceProvider();
var sqlHouse = serviceProvider.GetService<MsSqlHouseRepository>();
var googleHouse = serviceProvider.GetService<FromGoogleHouseRepository>();
//測試區專用模擬資料
var mockHouse = serviceProvider.GetService<MockHouseRepository>();

//依據參數對應去注入符合情境的BuildingContext實例
builder.Services.AddTransient(sp =>
{
    //若是測試區改用測試專用資料
    if (env.Equals("Development"))
        return new BuildingContext(mockHouse, mockHouse);

    return new BuildingContext(sqlHouse, googleHouse);

});

在Sinyi.Net可以透過不同環境的設定檔設計對應的注入階層,服務會根據環境去讀取對應設定檔並做後續依賴注入動作。
image

Sinyi.Net套件注入

在Sinyi.Net除了底層套件之外,也可以透過功能套件的擴充來協助專案開發。而套件的設定與使用也是透過設定檔注入方式做處理,設定檔分為依據環境分類的Sinyi.DevOps.Hosting.json跟預設的appsettings.json

  • Sinyi.DevOps.Hosting.json: 放會根據環境而有內容變化的套件設定,例如資料庫連線或api連線。
  • appsettings.json: 不會依據環境變化的共同功能套件,例如swagger或是Token驗證。

以MsSql連線套件Sinyi.Data.MsSqlClient.Hosting為例:

先根據環境資料夾的設定檔添加套件注入對應階層


  //mssql連線套件
  "Sinyi.Data.MsSqlClient": {
    "MsSqlClientFactory": {
      "ConnectionStrings": {
        //建立不同的key在access層進行連線抽換
        "DefaultConnection": "sql connection string",
        "third-party": "sql connection string"
      }
    }
  }

在使用的模組建構子直接增添套件物件:

 [RegisterService<IHouseRepository>()]
    public class MsSqlHouseRepository : IHouseRepository
    {
        private MsSqlClientFactory _sqlClientFactory;
        // Constructors
        //建構子加了MsSqlClientFactory進行資料庫連線動作
        public MsSqlHouseRepository(MsSqlClientFactory sqlClientFactory)
        {
            _sqlClientFactory = sqlClientFactory;
        }

        // Methods
        public House GetHouse()
        {
            //sql command
            using (var cmd = _sqlClientFactory.CreateCommand("DefaultConnection"))
            {
                cmd.CommandText = "SELECT 1;";
                var result = cmd.ExecuteNonQuery();
            }

            var house = new House() { Name = "Sql House" };
            return house;
        }
    }

第三方套件或自建功能依賴注入

若Sinyi.Net提供的功能套件或本身的底層設定不夠滿足開發專案的需求,開發者也能透過Sinyi.AspNetCore.Hosting(用於AspNetCore架構)Sinyi.NetCore.Hosting(用於NetCore架構) 的注入套件來擴充自己服務。

假設情境是我們因為特殊需求需要擴充Newtonsoft.Json來替代原生的System.Text.Json,勢必先需取用其套件並進行設定。

在這之前我們可以建立一個專門放注入相關邏輯的並以專案名為開頭的命名,例如Sinyi.Building的專案因有擴充需求而有了Sinyi.Building.Hosting的專案檔。
image

並根據參考此專案的起始檔是否為AspNetCore而選擇注入套件。以此專案為例因為是開發API故選擇Sinyi.AspNetCore.Hosting來進行注入設計。
image
再在以此專案安裝Microsoft.AspNetCore.Mvc.NewtonsoftJson等等在注入端使用其設定方法。

專案檔安裝完所需套件後我們需要準備兩個檔案:

一個是以此套件名稱+builder的cs檔來作為此套件建立的來源,以目前例子來說我們建立一個NewtonsoftJsonBuilder.cs的檔案。
另一個可以是以此套件名稱+Setting的cs檔來作為此套件的設定檔,以目前例子我們建立一個NewtonsoftJsonSetting.cs的檔案。

我們在NewtonsoftJsonBuilder內去繼承注入套件中的ServiceBuilder,並在其泛型中放入NewtonsoftJsonSetting

### NewtonJsonBuilder.cs檔 ###

using Microsoft.AspNetCore.Builder;
using Sinyi.AspNetCore.Hosting;

namespace Sinyi.Building.Hosting
{
    public class NewtonJsonBuilder : ServiceBuilder<NewtonsoftJsonSetting>
    {
        //建構子必備且須帶入設定檔階層名稱
        public NewtonJsonBuilder(string @namespace, string service = null) : base(@namespace, service)
        {
        }

        //注入邏輯用來設定套件參數的地方
        public override void ConfigureService(WebApplicationBuilder builder, NewtonJsonBuilderSetting setting)
        {
            throw new NotImplementedException();
        }
    }
}

在套件設定檔這我們設個調整時間格式的字串屬性

### NewtonJsonBuilderSetting.cs檔 ###

 public class NewtonJsonBuilderSetting
{
      public string DataFormat { get; set; }
}

再來我們決定此套件注入的設定檔:
我們在Sinyi.DevOps.Hosting.json或預設的appsettings.json根據這套件注入使否根據環境而有變異來決定注入設定放在哪。以此為例假設各環境的Json設定都一樣我們放在appsettings.json就好。

我們在設定檔組一個名為NewtonJson的階層並把剛剛於NewtonJsonBuilderSetting的屬性名稱設定為key並帶入我們想注入的value。(顧名思義設定檔階層內的內容須與builder那放在泛型的setting.cs內容屬性一樣)


"NewtonJson": {
    "DataFormat": "MMMM dd, yyyy"
  }

回到NewtonJsonBuilder我們開始把設定檔的設定與套件方法帶進來:

  1. 需在builder建構子的base帶入設定檔階層名稱,假設階層有兩個base內左邊參數(namespace)先放第一階層右邊參數(service) 再放下個階層。
  2. ConfigureService內調用WebApplicationBuilder內的Services去增添NewtonsoftJson方法。
  3. 將setting內的屬性與套件的屬性去匹配,達到透過注入設定檔調整套件設定。
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Sinyi.AspNetCore.Hosting;

namespace Sinyi.Building.Hosting
{
    public class NewtonJsonBuilder : ServiceBuilder<NewtonJsonBuilderSetting>
    {
        //建構子必備且須帶入設定檔階層名稱
        public NewtonJsonBuilder() : base("NewtonJson") {}

        public override void ConfigureService(WebApplicationBuilder builder, NewtonJsonBuilderSetting setting)
        {
            //WebApplicationBuilder調用到ServiceCollection並利用AddControllers去使用AddNewtonsoftJson
            builder.Services.AddControllers().AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.DateFormatString = setting.DataFormat;
            });
        }
    }
}

最後在測試看看透過builder﹑setting與對應設定檔是否有將設定值傳到buillder內進行注入邏輯設定。
image

依賴注入總結

  • Sinyi.Net的依賴注入依靠設定檔的階層進行註冊,且依據環境是否改變內容而新增至對應環境設定檔或共用設定檔。注入有時失敗很多都是設定檔名稱打錯或無法對應這部分需多加注意。
  • 不管使用Sinti.Registration做模組注入或是用Hosting注入套件自行注入擴充功能,起始檔皆需參考被註冊的專案檔,才能抓到註冊的物件。

起始專案介紹

Sinyi.Net起始專案分為Api為主的專案與Web為主的專案,皆使用Sinyi.AspNetCore進行設定包裝與套件注入。其中差異在於Api須加上Swagger與ApiVersion的功能,而Web可使用StaffAuth的驗證服務等網站驗證功能。

API

API版本預設套用Sinyi.AspNetCore.SwaggerGen.HostingSinyi.AspNetCore.Versioning.Hosting作為swaggerapi版本控制的處理。
image

在控制器中必須添加Module屬性,用來分類服務網域
若Module沒有帶入任何值,則下圖網域首頁即為:Domain/Home/Index
假設Modile帶入City的話,則網域首頁即為: Domain/City/Home/Index

而在每個API的方法之上需添加ApiVersion(系統內建)的屬性,為該API作版本控制。
image

假設需求有同名方法且有不同api版本,需在其同名方法上添加屬性ActionName並帶入原本方法名,而同名方法須改可以辨別的名稱。
image

最後可以在Swagger中選擇不同api版本進行swagger測試:
image

Web

Web版本跟API版本除了多了View的部分,也可以套用Sinyi.StaffAuth套件來進行員工驗證。
此功能會在另開一版Sinyi.StaffAuth服務作介紹。(未來把連結貼於此)

Sinyi.Net功能套件清單

待補充(將逐一寫新文章詳細介紹)

Sinyi.Net平台清單

Sinyi Notifcation 推播服務

Select a repo