Kondi Coding
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee
  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 前言 Sinyi.Net協助開發人員快速建立Web站台、API服務和Console程式。提供參數管理、依賴注入、身分驗證等多種功能模組,用以簡化開發流程並滿足多變的商業需求。 # 架構流程 ![Sinyi.Net架構圖.drawio (2)](https://hackmd.io/_uploads/BJqlETJJC.png) Sinyi.Net架構主要分為 * **Presentation(表現層):** 服務對外的介面,常見的API介面或是Web畫面呈現跟背景程式等等. * **Domain(邏輯層):** 負責整個服務的核心邏輯,以架構預設為 * **Context:** 依需求去取資料並組合與判斷邏輯的核心區塊.**(必要)** * **Entity:** 針對服務的需求或特性去定義的物件實體,它有獨特識別符號,如ID,並且具有相應行為和狀態。實體是邏輯層的核心,用於表達領域中的重要概念和業務規則。**(必要)** * **Repository:** 將實體Entity的持久化操作抽象化,提供**邏輯層**與**資料層**之間的交互接口. * **Provider:** 負責將第三方的資料以實體去設計並提供給其他區塊. * **Service:** 若邏輯符合一個情境可設計成一組Service讓邏輯更簡化. 除了**Context**與**Entity**之外其他都是針對使用情境與條件做設計. * **Access(資料存取層):** 串接資料的階層,依據資料來源渠道進行命名. 外界透過**表現層**的接口執行動作,在**邏輯層**依據執行條件來匹配符合的邏輯。而在依據情境去使用對應的**實體**並產生行為,再透過行為去**資料層**進行所需的資料串接。 --- # 底層套件 Sinyi.Net架構下運作是依靠兩個底層套件做運行,一個是放置於起始檔的**Sinyi.AspNetCore/Sinyi.NetCore**跟放置在需依賴注入物件的其他專案檔**Sinyi.Registration**。 ![Sinyi.Net架構圖.drawio (3)](https://hackmd.io/_uploads/Syx92g_10.png) * **Sinyi.AspNetCore/Sinyi.NetCore:** 置於專案起始檔內,是Sinyi.Net的核心。負責專案啟動、依賴注入調用與其她底層功能封裝。 * **Sinyi.Registration:** 負責依賴注入的註冊,置於須手動進行依賴注入的專案檔。如何透過此套件進行注入後面會有說明。 # 依賴注入 依賴注入(DI)是一種設計模式,透過外部化依賴項目的方式,使程式碼更靈活、易測試、易維護。透過將依賴項目注入到類別中,提高了程式碼的鬆耦合性、可測試性、可維護性、靈活性和可擴展性。 而Sinyi.Net包裝並調整了原生.NetCore的依賴注入方式,透過設定檔的階層設計去進行註冊與調用。讓開發者專注於邏輯開發且透過靈活的注入方式為不同情境做架構設計。 ### 基本情境 我們先預設我們有一個負責建築服務的核心邏輯取名為BuildingContext,其方法GetHouse需使用為IHouseRepository的房子介面的實例MsSqlHouseRepository。可以得知其實例是從MsSql取得房子資訊。 關係如下圖: ![image](https://hackmd.io/_uploads/H1U0sLGyC.png) 在原生.NetCore會是在起始檔寫依賴注入的語法進行註冊。 ``` csharp 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的實例。 ``` csharp 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](https://hackmd.io/_uploads/BJTQOuZyR.png) 以BuildingContext為例其在建構子注入參數中有個命名為houseRepository的實例,故在以BuildingContext為命名的階層內新增個key名為HouseRepository(大小寫無區別)且value設為要使用實例的型態。 而所需使用的實例也必須註冊,故需以其型態為命名組合它的階層。 ```json { //請以專案名稱命名,底層會 "Sinyi.building": { // BuildingContext "BuildingContext": { //key為HouseRepository對應建構子內參數的名稱(不分大小寫),value為對應的實例type "HouseRepository": "MsSqlHouseRepository" }, // IHouseRepository "MsSqlHouseRepository": {} } } ``` 而在需註冊注入的實例需添加**RegisterService**屬性。 ```csharp 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的型態。 ```csharp 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](https://hackmd.io/_uploads/SJu6RIfy0.png) 其情境在在原生.NetCore的依賴注入語法稍有修改: ```csharp 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的調整: ```csharp= 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屬性。 ```csharp 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的建構子參數為例,在對應環境的設定檔做階層命名: ```json { "Sinyi.building": { // BuildingContext "BuildingContext": { //key為BuildingContext參數的名稱,於value為下面註冊的實例名稱(型態名稱) "sqlhouseRepository": "MsSqlHouseRepository", "fromGoogleRepository": "FromGoogleHouseRepository" }, // IHouseRepository "MsSqlHouseRepository": {}, "FromGoogleHouseRepository": {} } ``` ### 環境不同的情境 若情境會需要根據不同環境有不同的注入實例,像是僅測試區不能使用到資料庫與第三方資料,須自產假資料作提供跑完服務。那在原生的案例會需要在專案起始檔的部分針對注入做一些條件判斷。 ```csharp ///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](https://hackmd.io/_uploads/HJyL02G10.png) ### Sinyi.Net套件注入 在Sinyi.Net除了底層套件之外,也可以透過功能套件的擴充來協助專案開發。而套件的設定與使用也是透過設定檔注入方式做處理,設定檔分為依據環境分類的**Sinyi.DevOps.Hosting.json**跟預設的**appsettings.json**。 * **Sinyi.DevOps.Hosting.json:** 放會根據環境而有內容變化的套件設定,例如資料庫連線或api連線。 * **appsettings.json:** 不會依據環境變化的共同功能套件,例如swagger或是Token驗證。 以MsSql連線套件**Sinyi.Data.MsSqlClient.Hosting**為例: 先根據環境資料夾的設定檔添加套件注入對應階層 ```json //mssql連線套件 "Sinyi.Data.MsSqlClient": { "MsSqlClientFactory": { "ConnectionStrings": { //建立不同的key在access層進行連線抽換 "DefaultConnection": "sql connection string", "third-party": "sql connection string" } } } ``` 在使用的模組建構子直接增添套件物件: ```csharp [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](https://hackmd.io/_uploads/r1tCoRm1R.png) 並根據參考此專案的起始檔是否為AspNetCore而選擇注入套件。以此專案為例因為是開發API故選擇**Sinyi.AspNetCore.Hosting**來進行注入設計。 ![image](https://hackmd.io/_uploads/r1cprRQ1A.png) 再在以此專案安裝**Microsoft.AspNetCore.Mvc.NewtonsoftJson**等等在注入端使用其設定方法。 專案檔安裝完所需套件後我們需要準備兩個檔案: 一個是以此**套件名稱+builder**的cs檔來作為此套件建立的來源,以目前例子來說我們建立一個NewtonsoftJsonBuilder.cs的檔案。 另一個可以是以此**套件名稱+Setting**的cs檔來作為此套件的設定檔,以目前例子我們建立一個NewtonsoftJsonSetting.cs的檔案。 我們在**NewtonsoftJsonBuilder**內去繼承注入套件中的**ServiceBuilder**,並在其泛型中放入**NewtonsoftJsonSetting**。 ```csharp ### 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(); } } } ``` 在套件設定檔這我們設個調整時間格式的字串屬性 ```csharp ### 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內容屬性一樣) ```json "NewtonJson": { "DataFormat": "MMMM dd, yyyy" } ``` 回到**NewtonJsonBuilder**我們開始把設定檔的設定與套件方法帶進來: 1. 需在builder建構子的base帶入設定檔**階層名稱**,假設階層有兩個base內**左邊參數(namespace)**先放第一階層**右邊參數(service)** 再放下個階層。 2. 在**ConfigureService**內調用**WebApplicationBuilder內的Services**去增添**NewtonsoftJson**方法。 3. 將setting內的屬性與套件的屬性去匹配,達到透過注入設定檔調整套件設定。 ```csharp 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](https://hackmd.io/_uploads/H1tEwgVJ0.png) ### 依賴注入總結 * Sinyi.Net的依賴注入依靠**設定檔的階層**進行註冊,且依據環境是否改變內容而新增至對應環境設定檔或共用設定檔。***注入有時失敗很多都是設定檔名稱打錯或無法對應這部分需多加注意。*** * 不管使用**Sinti.Registration**做模組注入或是用**Hosting注入套件**自行注入擴充功能,起始檔皆需參考被註冊的專案檔,才能抓到註冊的物件。 # 起始專案介紹 Sinyi.Net起始專案分為**Api為主**的專案與**Web為主**的專案,皆使用**Sinyi.AspNetCore**進行設定包裝與套件注入。其中差異在於Api須加上Swagger與ApiVersion的功能,而Web可使用StaffAuth的驗證服務等網站驗證功能。 ### API API版本預設套用**Sinyi.AspNetCore.SwaggerGen.Hosting**與**Sinyi.AspNetCore.Versioning.Hosting**作為**swagger**與**api版本控制**的處理。 ![image](https://hackmd.io/_uploads/r1Lx1l_JR.png) 在控制器中必須添加**Module**屬性,用來分類服務網域 若Module沒有帶入任何值,則下圖網域首頁即為:Domain/Home/Index 假設Modile帶入City的話,則網域首頁即為: Domain/City/Home/Index 而在每個API的方法之上需添加**ApiVersion**(系統內建)的屬性,為該API作版本控制。 ![image](https://hackmd.io/_uploads/BJOXZxdy0.png) 假設需求有同名方法且有不同api版本,需在其同名方法上添加屬性**ActionName**並帶入原本方法名,而同名方法須改可以辨別的名稱。 ![image](https://hackmd.io/_uploads/rJ5QqxuJC.png) 最後可以在Swagger中選擇不同api版本進行swagger測試: ![image](https://hackmd.io/_uploads/r1K1oeOy0.png) ### Web Web版本跟API版本除了多了View的部分,也可以套用Sinyi.StaffAuth套件來進行員工驗證。 此功能會在另開一版**Sinyi.StaffAuth服務**作介紹。(未來把連結貼於此) # Sinyi.Net功能套件清單 待補充(將逐一寫新文章詳細介紹) # Sinyi.Net平台清單 [Sinyi Notifcation 推播服務](https://sinyicomtw.sharepoint.com/sites/techcommittee/SitePages/Sinyi-Notification.aspx)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully