[{"data":1,"prerenderedAt":1181},["ShallowReactive",2],{"portfolio-\u002Fportfolio\u002Feventbus-dotnet":3},{"id":4,"title":5,"award":6,"body":7,"cover":1160,"description":49,"extension":1161,"featured":1162,"link":6,"meta":1163,"navigation":545,"order":454,"path":1164,"pinned":1162,"repo":1165,"role":1166,"seo":1167,"stack":1168,"stem":1177,"summary":1178,"year":1179,"__hash__":1180},"portfolio\u002Fportfolio\u002Feventbus-dotnet.md","EventBus（.NET）",null,{"type":8,"value":9,"toc":1151},"minimark",[10,14,36,43,106,109,116,123,136,143,147,154,172,240,243,306,317,320,335,640,643,679,689,692,698,722,728,752,863,868,943,953,1037,1040,1119,1122,1147],[11,12,13],"h2",{"id":13},"專案概述",[15,16,17,18,22,23,27,28,31,32,35],"p",{},"一個小型開源函式庫，把 .NET 中的",[19,20,21],"strong",{},"事件發布與訂閱（Pub\u002FSub）抽象化","：應用程式\n只需要面對 ",[24,25,26],"code",{},"IEventBus"," 和 ",[24,29,30],{},"IEventHandler\u003CT>"," 兩個介面，不需要在意事件是透過記\n憶體派送，還是透過 RabbitMQ 傳遞——",[19,33,34],{},"換傳輸方式只需要換一行 DI 設定","，不需要改\n任何業務程式碼。",[15,37,38,39,42],{},"已透過 ",[24,40,41],{},"GeneratePackageOnBuild"," 發布成三個獨立的 NuGet 套件：",[44,45,50],"pre",{"className":46,"code":47,"language":48,"meta":49,"style":49},"language-shell shiki shiki-themes github-dark","Install-Package EventBus -Version 0.0.1-beta            # 核心介面\nInstall-Package EventBus.InMemory -Version 0.0.1-beta    # 記憶體傳輸\nInstall-Package EventBus.RabbitMQ -Version 0.0.1-beta    # RabbitMQ 傳輸（底層為 EasyNetQ）\n","shell","",[24,51,52,76,91],{"__ignoreMap":49},[53,54,57,61,65,69,72],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"svObZ","Install-Package",[53,62,64],{"class":63},"sU2Wk"," EventBus",[53,66,68],{"class":67},"sDLfK"," -Version",[53,70,71],{"class":63}," 0.0.1-beta",[53,73,75],{"class":74},"sAwPA","            # 核心介面\n",[53,77,79,81,84,86,88],{"class":55,"line":78},2,[53,80,60],{"class":59},[53,82,83],{"class":63}," EventBus.InMemory",[53,85,68],{"class":67},[53,87,71],{"class":63},[53,89,90],{"class":74},"    # 記憶體傳輸\n",[53,92,94,96,99,101,103],{"class":55,"line":93},3,[53,95,60],{"class":59},[53,97,98],{"class":63}," EventBus.RabbitMQ",[53,100,68],{"class":67},[53,102,71],{"class":63},[53,104,105],{"class":74},"    # RabbitMQ 傳輸（底層為 EasyNetQ）\n",[11,107,108],{"id":108},"為什麼做這個",[15,110,111,112,115],{},"在事件驅動的專案裡，Pub\u002FSub 幾乎每個服務都會用到，但每次都直接寫 RabbitMQ 的程\n式碼，業務邏輯就會和訊息佇列綁在一起：開發、單元測試時要先架好一台 RabbitMQ，\n想換成別的傳輸方式也要整個重寫。所以我想要的是一個",[19,113,114],{},"抽象化的 Pub\u002FSub","——業務\n程式碼只說「發布這個事件」「我要處理這個事件」，至於事件怎麼送過去，交給底層決\n定。",[15,117,118,119,122],{},"市面上當然有現成的選擇，例如 Azure Service Bus 的 SDK、MassTransit，但實際評估\n後發現它們對我的需求來說",[19,120,121],{},"都做得太複雜了","：",[124,125,126,130,133],"ul",{},[127,128,129],"li",{},"功能涵蓋 Saga、排程訊息、請求／回應、多種傳輸的細部設定……真正會用到的只有\n「發布」和「訂閱」，卻得先理解一整套框架的概念與設定方式",[127,131,132],{},"框架本身的抽象層很厚，出問題時要追到框架內部才知道訊息卡在哪裡",[127,134,135],{},"團隊裡的每個人都要先學會這套框架，才能開始寫一個簡單的事件處理",[15,137,138,139,142],{},"我需要的其實只是一層很薄的抽象：",[19,140,141],{},"介面夠少、行為好預測、程式碼讀得完","。所以就\n自己做了一個，把實務上反覆用到的 Pub\u002FSub 模式抽出來，發布成獨立的函式庫，之後的\n專案直接引用。",[11,144,146],{"id":145},"設計概念比照-servicecollection","設計概念：比照 ServiceCollection",[15,148,149],{},[150,151],"img",{"alt":152,"src":153},"EventBus 訂閱機制示意圖：啟動時 AddEventHandlersFromAssembly 以反射掃描組件，找出實作 IEventHandler 泛型介面的類別並取出事件型別，登記到 SubscriptionCollection；執行時 IEventBus 發布事件經過傳輸層，背景服務依事件型別查表，從 DI 取出對應的 Handler 並呼叫 HandleAsync","\u002Fimages\u002Fportfolio\u002Feventbus-subscription.svg",[15,155,156,157,160,161,164,165,171],{},".NET 開發者每天都在用 ",[24,158,159],{},"IServiceCollection","：啟動時把服務註冊進去，執行時由\n",[24,162,163],{},"ServiceProvider"," 依型別解析出實例。我希望訂閱事件的方式也一樣直覺，所以",[19,166,167,168],{},"比照\nServiceCollection 的設計，多做了一個 ",[24,169,170],{},"SubscriptionCollection"," 專門管理訂閱\n者：",[173,174,175,189],"table",{},[176,177,178],"thead",{},[179,180,181,184,187],"tr",{},[182,183],"th",{},[182,185,186],{},"ServiceCollection",[182,188,170],{},[190,191,192,204,214,225],"tbody",{},[179,193,194,198,201],{},[195,196,197],"td",{},"登記的是什麼",[195,199,200],{},"服務型別 → 實作型別",[195,202,203],{},"事件型別 → 訂閱者（Handler）型別",[179,205,206,209,212],{},[195,207,208],{},"什麼時候登記",[195,210,211],{},"應用程式啟動時",[195,213,211],{},[179,215,216,219,222],{},[195,217,218],{},"什麼時候用到",[195,220,221],{},"需要服務時解析出實例",[195,223,224],{},"收到事件時找出要通知哪些 Handler",[179,226,227,230,235],{},[195,228,229],{},"註冊方式",[195,231,232],{},[24,233,234],{},"services.AddScoped\u003CIFoo, Foo>()",[195,236,237],{},[24,238,239],{},"s.AddEventHandlersFromAssembly()",[15,241,242],{},"使用者寫起來就和註冊一般服務沒有兩樣：",[44,244,248],{"className":245,"code":246,"language":247,"meta":49,"style":49},"language-csharp shiki shiki-themes github-dark","builder.Services.AddEventBus(o => o\n    .UseSubscriptions(s => s.AddEventHandlersFromAssembly())\n    .UseInMemory());\n","csharp",[24,249,250,272,296],{"__ignoreMap":49},[53,251,252,256,259,262,265,269],{"class":55,"line":56},[53,253,255],{"class":254},"s95oV","builder.Services.",[53,257,258],{"class":59},"AddEventBus",[53,260,261],{"class":254},"(",[53,263,264],{"class":59},"o",[53,266,268],{"class":267},"snl16"," =>",[53,270,271],{"class":254}," o\n",[53,273,274,277,280,282,285,287,290,293],{"class":55,"line":78},[53,275,276],{"class":254},"    .",[53,278,279],{"class":59},"UseSubscriptions",[53,281,261],{"class":254},[53,283,284],{"class":59},"s",[53,286,268],{"class":267},[53,288,289],{"class":254}," s.",[53,291,292],{"class":59},"AddEventHandlersFromAssembly",[53,294,295],{"class":254},"())\n",[53,297,298,300,303],{"class":55,"line":93},[53,299,276],{"class":254},[53,301,302],{"class":59},"UseInMemory",[53,304,305],{"class":254},"());\n",[15,307,308,310,311,313,314,316],{},[24,309,170],{}," 只負責記錄「哪個事件要交給哪些 Handler」，Handler 本\n身仍然註冊在 .NET 原本的 ",[24,312,159],{}," 裡。收到事件時，先查\n",[24,315,170],{}," 找出 Handler 型別，再從 DI 建立實例——所以 Handler 一\n樣可以用建構子注入 Repository、Logger 等任何服務，生命週期也交給 DI 管理。",[11,318,319],{"id":319},"以反射自動找出訂閱者",[15,321,322,323,326,327,330,331,334],{},"如果每新增一個事件處理器，都要回到 ",[24,324,325],{},"Program.cs"," 手動寫一行 ",[24,328,329],{},"Subscribe","，久了一\n定會有人忘記。所以訂閱的登記交給",[19,332,333],{},"反射","自動完成：",[44,336,338],{"className":245,"code":337,"language":247,"meta":49,"style":49},"\u002F\u002F 示意：掃描組件，找出所有實作 IEventHandler\u003CT> 的類別\npublic SubscriptionCollection AddEventHandlersFromAssembly(Assembly assembly)\n{\n    var handlers = assembly.GetTypes()\n        .Where(t => t is { IsClass: true, IsAbstract: false })\n        .SelectMany(t => t.GetInterfaces()\n            .Where(i => i.IsGenericType &&\n                        i.GetGenericTypeDefinition() == typeof(IEventHandler\u003C>))\n            .Select(i => (EventType: i.GetGenericArguments()[0], HandlerType: t)));\n\n    foreach (var (eventType, handlerType) in handlers)\n    {\n        Add(eventType, handlerType);              \u002F\u002F 登記到 SubscriptionCollection\n        _services.AddScoped(handlerType);         \u002F\u002F Handler 本身交給 DI 管理\n    }\n\n    return this;\n}\n",[24,339,340,345,367,372,393,430,452,473,499,540,547,578,584,596,611,617,622,634],{"__ignoreMap":49},[53,341,342],{"class":55,"line":56},[53,343,344],{"class":74},"\u002F\u002F 示意：掃描組件，找出所有實作 IEventHandler\u003CT> 的類別\n",[53,346,347,350,353,356,358,361,364],{"class":55,"line":78},[53,348,349],{"class":267},"public",[53,351,352],{"class":59}," SubscriptionCollection",[53,354,355],{"class":59}," AddEventHandlersFromAssembly",[53,357,261],{"class":254},[53,359,360],{"class":59},"Assembly",[53,362,363],{"class":59}," assembly",[53,365,366],{"class":254},")\n",[53,368,369],{"class":55,"line":93},[53,370,371],{"class":254},"{\n",[53,373,375,378,381,384,387,390],{"class":55,"line":374},4,[53,376,377],{"class":267},"    var",[53,379,380],{"class":59}," handlers",[53,382,383],{"class":267}," =",[53,385,386],{"class":254}," assembly.",[53,388,389],{"class":59},"GetTypes",[53,391,392],{"class":254},"()\n",[53,394,396,399,402,404,407,409,412,415,418,421,424,427],{"class":55,"line":395},5,[53,397,398],{"class":254},"        .",[53,400,401],{"class":59},"Where",[53,403,261],{"class":254},[53,405,406],{"class":59},"t",[53,408,268],{"class":267},[53,410,411],{"class":254}," t ",[53,413,414],{"class":267},"is",[53,416,417],{"class":254}," { IsClass: ",[53,419,420],{"class":67},"true",[53,422,423],{"class":254},", IsAbstract: ",[53,425,426],{"class":67},"false",[53,428,429],{"class":254}," })\n",[53,431,433,435,438,440,442,444,447,450],{"class":55,"line":432},6,[53,434,398],{"class":254},[53,436,437],{"class":59},"SelectMany",[53,439,261],{"class":254},[53,441,406],{"class":59},[53,443,268],{"class":267},[53,445,446],{"class":254}," t.",[53,448,449],{"class":59},"GetInterfaces",[53,451,392],{"class":254},[53,453,455,458,460,462,465,467,470],{"class":55,"line":454},7,[53,456,457],{"class":254},"            .",[53,459,401],{"class":59},[53,461,261],{"class":254},[53,463,464],{"class":59},"i",[53,466,268],{"class":267},[53,468,469],{"class":254}," i.IsGenericType ",[53,471,472],{"class":267},"&&\n",[53,474,476,479,482,485,488,491,493,496],{"class":55,"line":475},8,[53,477,478],{"class":254},"                        i.",[53,480,481],{"class":59},"GetGenericTypeDefinition",[53,483,484],{"class":254},"() ",[53,486,487],{"class":267},"==",[53,489,490],{"class":267}," typeof",[53,492,261],{"class":254},[53,494,495],{"class":59},"IEventHandler",[53,497,498],{"class":254},"\u003C>))\n",[53,500,502,504,507,509,511,513,516,519,522,525,528,531,534,537],{"class":55,"line":501},9,[53,503,457],{"class":254},[53,505,506],{"class":59},"Select",[53,508,261],{"class":254},[53,510,464],{"class":59},[53,512,268],{"class":267},[53,514,515],{"class":254}," (",[53,517,518],{"class":59},"EventType",[53,520,521],{"class":254},": i.",[53,523,524],{"class":59},"GetGenericArguments",[53,526,527],{"class":254},"()[",[53,529,530],{"class":67},"0",[53,532,533],{"class":254},"], ",[53,535,536],{"class":59},"HandlerType",[53,538,539],{"class":254},": t)));\n",[53,541,543],{"class":55,"line":542},10,[53,544,546],{"emptyLinePlaceholder":545},true,"\n",[53,548,550,553,555,558,560,563,566,569,572,575],{"class":55,"line":549},11,[53,551,552],{"class":267},"    foreach",[53,554,515],{"class":254},[53,556,557],{"class":267},"var",[53,559,515],{"class":254},[53,561,562],{"class":59},"eventType",[53,564,565],{"class":254},", ",[53,567,568],{"class":59},"handlerType",[53,570,571],{"class":254},") ",[53,573,574],{"class":267},"in",[53,576,577],{"class":254}," handlers)\n",[53,579,581],{"class":55,"line":580},12,[53,582,583],{"class":254},"    {\n",[53,585,587,590,593],{"class":55,"line":586},13,[53,588,589],{"class":59},"        Add",[53,591,592],{"class":254},"(eventType, handlerType);              ",[53,594,595],{"class":74},"\u002F\u002F 登記到 SubscriptionCollection\n",[53,597,599,602,605,608],{"class":55,"line":598},14,[53,600,601],{"class":254},"        _services.",[53,603,604],{"class":59},"AddScoped",[53,606,607],{"class":254},"(handlerType);         ",[53,609,610],{"class":74},"\u002F\u002F Handler 本身交給 DI 管理\n",[53,612,614],{"class":55,"line":613},15,[53,615,616],{"class":254},"    }\n",[53,618,620],{"class":55,"line":619},16,[53,621,546],{"emptyLinePlaceholder":545},[53,623,625,628,631],{"class":55,"line":624},17,[53,626,627],{"class":267},"    return",[53,629,630],{"class":67}," this",[53,632,633],{"class":254},";\n",[53,635,637],{"class":55,"line":636},18,[53,638,639],{"class":254},"}\n",[15,641,642],{},"流程是：",[644,645,646,652,659,673],"ol",{},[127,647,648,649],{},"列出組件中所有",[19,650,651],{},"非抽象的類別",[127,653,654,655,658],{},"檢查它實作的介面，找出 ",[24,656,657],{},"IEventHandler\u003C>"," 這個泛型介面",[127,660,661,662,665,666,669,670],{},"用 ",[24,663,664],{},"GetGenericArguments()"," 取出泛型參數 ",[24,667,668],{},"T","，也就是",[19,671,672],{},"它要處理的事件型別",[127,674,675,676,678],{},"把「事件型別 → Handler 型別」登記到 ",[24,677,170],{},"，同時把 Handler\n註冊進 DI",[15,680,681,682,684,685,688],{},"一個類別也可以同時實作多個 ",[24,683,30],{},"，處理多種事件；同一個事件也可以有\n多個 Handler 訂閱，發布時會逐一通知。反射只在",[19,686,687],{},"啟動時","跑一次，執行期間派送事件\n只是查表，不會有額外的效能負擔。",[11,690,691],{"id":691},"使用方式",[15,693,694,697],{},[19,695,696],{},"註冊（記憶體版）","：適合單一服務內的事件，或開發、測試時使用",[44,699,701],{"className":245,"code":700,"language":247,"meta":49,"style":49},"builder.Services.AddEventBus(o => o.UseInMemory());\n",[24,702,703],{"__ignoreMap":49},[53,704,705,707,709,711,713,715,718,720],{"class":55,"line":56},[53,706,255],{"class":254},[53,708,258],{"class":59},[53,710,261],{"class":254},[53,712,264],{"class":59},[53,714,268],{"class":267},[53,716,717],{"class":254}," o.",[53,719,302],{"class":59},[53,721,305],{"class":254},[15,723,724,727],{},[19,725,726],{},"註冊（RabbitMQ 版）","：跨服務傳遞事件",[44,729,731],{"className":245,"code":730,"language":247,"meta":49,"style":49},"builder.Services.AddEventBus(o => o.UseRabbitMQ());\n",[24,732,733],{"__ignoreMap":49},[53,734,735,737,739,741,743,745,747,750],{"class":55,"line":56},[53,736,255],{"class":254},[53,738,258],{"class":59},[53,740,261],{"class":254},[53,742,264],{"class":59},[53,744,268],{"class":267},[53,746,717],{"class":254},[53,748,749],{"class":59},"UseRabbitMQ",[53,751,305],{"class":254},[44,753,757],{"className":754,"code":755,"language":756,"meta":49,"style":49},"language-json shiki shiki-themes github-dark","{\n  \"RabbitMqOption\": {\n    \"ExchangeName\": \"sample.exchange\",\n    \"QueueName\": \"sample.queue\",\n    \"ConnectionString\": \"localhost:5672\",\n    \"PublishRetryCount\": 3,\n    \"SubscribeRetryTime\": 3,\n    \"MaxPriority\": 10,\n    \"ConsumePrefetchCount\": 2\n  }\n}\n","json",[24,758,759,763,771,785,797,809,821,832,844,854,859],{"__ignoreMap":49},[53,760,761],{"class":55,"line":56},[53,762,371],{"class":254},[53,764,765,768],{"class":55,"line":78},[53,766,767],{"class":67},"  \"RabbitMqOption\"",[53,769,770],{"class":254},": {\n",[53,772,773,776,779,782],{"class":55,"line":93},[53,774,775],{"class":67},"    \"ExchangeName\"",[53,777,778],{"class":254},": ",[53,780,781],{"class":63},"\"sample.exchange\"",[53,783,784],{"class":254},",\n",[53,786,787,790,792,795],{"class":55,"line":374},[53,788,789],{"class":67},"    \"QueueName\"",[53,791,778],{"class":254},[53,793,794],{"class":63},"\"sample.queue\"",[53,796,784],{"class":254},[53,798,799,802,804,807],{"class":55,"line":395},[53,800,801],{"class":67},"    \"ConnectionString\"",[53,803,778],{"class":254},[53,805,806],{"class":63},"\"localhost:5672\"",[53,808,784],{"class":254},[53,810,811,814,816,819],{"class":55,"line":432},[53,812,813],{"class":67},"    \"PublishRetryCount\"",[53,815,778],{"class":254},[53,817,818],{"class":67},"3",[53,820,784],{"class":254},[53,822,823,826,828,830],{"class":55,"line":454},[53,824,825],{"class":67},"    \"SubscribeRetryTime\"",[53,827,778],{"class":254},[53,829,818],{"class":67},[53,831,784],{"class":254},[53,833,834,837,839,842],{"class":55,"line":475},[53,835,836],{"class":67},"    \"MaxPriority\"",[53,838,778],{"class":254},[53,840,841],{"class":67},"10",[53,843,784],{"class":254},[53,845,846,849,851],{"class":55,"line":501},[53,847,848],{"class":67},"    \"ConsumePrefetchCount\"",[53,850,778],{"class":254},[53,852,853],{"class":67},"2\n",[53,855,856],{"class":55,"line":542},[53,857,858],{"class":254},"  }\n",[53,860,861],{"class":55,"line":549},[53,862,639],{"class":254},[15,864,865],{},[19,866,867],{},"發布事件",[44,869,871],{"className":245,"code":870,"language":247,"meta":49,"style":49},"app.MapGet(\"\u002Fweatherforecast\", async (IEventBus eventBus) =>\n{\n    await eventBus.PublishAsync(new TestEvent { Message = \"Test\" });\n});\n",[24,872,873,903,907,938],{"__ignoreMap":49},[53,874,875,878,881,883,886,888,891,893,895,898,900],{"class":55,"line":56},[53,876,877],{"class":254},"app.",[53,879,880],{"class":59},"MapGet",[53,882,261],{"class":254},[53,884,885],{"class":63},"\"\u002Fweatherforecast\"",[53,887,565],{"class":254},[53,889,890],{"class":267},"async",[53,892,515],{"class":254},[53,894,26],{"class":59},[53,896,897],{"class":59}," eventBus",[53,899,571],{"class":254},[53,901,902],{"class":267},"=>\n",[53,904,905],{"class":55,"line":78},[53,906,371],{"class":254},[53,908,909,912,915,918,920,923,926,929,932,935],{"class":55,"line":93},[53,910,911],{"class":267},"    await",[53,913,914],{"class":254}," eventBus.",[53,916,917],{"class":59},"PublishAsync",[53,919,261],{"class":254},[53,921,922],{"class":267},"new",[53,924,925],{"class":59}," TestEvent",[53,927,928],{"class":254}," { Message ",[53,930,931],{"class":267},"=",[53,933,934],{"class":63}," \"Test\"",[53,936,937],{"class":254}," });\n",[53,939,940],{"class":55,"line":374},[53,941,942],{"class":254},"});\n",[15,944,945,948,949,952],{},[19,946,947],{},"訂閱事件","：實作 ",[24,950,951],{},"IEventHandler\u003CTEvent>"," 就好，啟動時會被自動掃描登記",[44,954,956],{"className":245,"code":955,"language":247,"meta":49,"style":49},"public class TestEventHandler : IEventHandler\u003CTestEvent>\n{\n    public Task HandleAsync(TestEvent @event)\n    {\n        Console.WriteLine(@event.Message);\n        return Task.CompletedTask;\n    }\n}\n",[24,957,958,982,986,1006,1010,1021,1029,1033],{"__ignoreMap":49},[53,959,960,962,965,968,971,973,976,979],{"class":55,"line":56},[53,961,349],{"class":267},[53,963,964],{"class":267}," class",[53,966,967],{"class":59}," TestEventHandler",[53,969,970],{"class":254}," : ",[53,972,495],{"class":59},[53,974,975],{"class":254},"\u003C",[53,977,978],{"class":59},"TestEvent",[53,980,981],{"class":254},">\n",[53,983,984],{"class":55,"line":78},[53,985,371],{"class":254},[53,987,988,991,994,997,999,1001,1004],{"class":55,"line":93},[53,989,990],{"class":267},"    public",[53,992,993],{"class":59}," Task",[53,995,996],{"class":59}," HandleAsync",[53,998,261],{"class":254},[53,1000,978],{"class":59},[53,1002,1003],{"class":59}," @event",[53,1005,366],{"class":254},[53,1007,1008],{"class":55,"line":374},[53,1009,583],{"class":254},[53,1011,1012,1015,1018],{"class":55,"line":395},[53,1013,1014],{"class":254},"        Console.",[53,1016,1017],{"class":59},"WriteLine",[53,1019,1020],{"class":254},"(@event.Message);\n",[53,1022,1023,1026],{"class":55,"line":432},[53,1024,1025],{"class":267},"        return",[53,1027,1028],{"class":254}," Task.CompletedTask;\n",[53,1030,1031],{"class":55,"line":454},[53,1032,616],{"class":254},[53,1034,1035],{"class":55,"line":475},[53,1036,639],{"class":254},[11,1038,1039],{"id":1039},"實作內容",[124,1041,1042,1059,1074,1088,1101,1104],{},[127,1043,1044,1045,1047,1048,1050,1051,1054,1055,1058],{},"抽象化 ",[24,1046,26],{}," \u002F ",[24,1049,30],{}," 介面，",[24,1052,1053],{},"EventBus.InMemory"," 與\n",[24,1056,1057],{},"EventBus.RabbitMq"," 分別實作記憶體與 RabbitMQ（底層包裝 EasyNetQ）兩種傳輸",[127,1060,1061,1062,1064,1065,1067,1068,1071,1072],{},"比照 ",[24,1063,159],{}," 設計 ",[24,1066,170],{}," 管理訂閱者，並提供\n",[24,1069,1070],{},"AddEventHandlersFromAssembly()"," 以反射自動掃描、登記所有 ",[24,1073,30],{},[127,1075,1076,1077,122,1080,1083,1084,1087],{},"RabbitMQ 版本支援以 ",[19,1078,1079],{},"Attribute 宣告事件的路由設定",[24,1081,1082],{},"[Exchange]"," 指定\nExchange 名稱與類型、",[24,1085,1086],{},"[Event]"," 指定訊息優先權（Priority）與事件版本\n（Version），路由設定跟著事件類別走，不用另外維護一份對照表",[127,1089,1090,1047,1093,1096,1097,1100],{},[24,1091,1092],{},"SubscriptionBackgroundService",[24,1094,1095],{},"SubscriberBackgroundService"," 以背景服務\n持續消費訊息，",[24,1098,1099],{},"BackgroundQueue"," 處理記憶體版本的非同步派送",[127,1102,1103],{},"發布與消費都可設定重試次數，RabbitMQ 版本另外支援訊息優先權與 Prefetch 數量",[127,1105,1106,1107,1110,1111,1114,1115,1118],{},"為兩種傳輸方式撰寫單元測試（",[24,1108,1109],{},"EventBus.Tests","、",[24,1112,1113],{},"EventBus.InMemory.Tests","）與\n整合測試（",[24,1116,1117],{},"EventBus.InMemory.IntegrationTests","）",[11,1120,1121],{"id":1121},"成果",[124,1123,1124,1132,1135,1138],{},[127,1125,1126,1127,1047,1129,1131],{},"業務程式碼只依賴 ",[24,1128,26],{},[24,1130,30],{},"，傳輸方式從記憶體換成\nRabbitMQ 只需要改一行設定",[127,1133,1134],{},"單元測試與本機開發改用記憶體版本，不需要先架 RabbitMQ",[127,1136,1137],{},"新增事件處理器只要實作介面，不需要再回頭修改註冊程式碼",[127,1139,1140,1141,1146],{},"後來 ",[1142,1143,1145],"a",{"href":1144},"\u002Fportfolio\u002Fecommerce-microservices-observability","電商微服務可觀測性專案","\n裡的事件驅動設計，也是以這套函式庫的概念為基礎",[1148,1149,1150],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":49,"searchDepth":78,"depth":78,"links":1152},[1153,1154,1155,1156,1157,1158,1159],{"id":13,"depth":78,"text":13},{"id":108,"depth":78,"text":108},{"id":145,"depth":78,"text":146},{"id":319,"depth":78,"text":319},{"id":691,"depth":78,"text":691},{"id":1039,"depth":78,"text":1039},{"id":1121,"depth":78,"text":1121},"\u002Fimages\u002Fportfolio\u002Feventbus-cover.svg","md",false,{},"\u002Fportfolio\u002Feventbus-dotnet","https:\u002F\u002Fgithub.com\u002Ftom3019\u002FEventBus","Side Project ‧ 開源函式庫",{"title":5,"description":49},[1169,1170,1171,1172,1173,1174,1175,1176],".NET 8","Pub\u002FSub","RabbitMQ","EasyNetQ","Dependency Injection","Reflection","xUnit","NuGet","portfolio\u002Feventbus-dotnet","一個輕量的 Pub\u002FSub 抽象層，已發布為 NuGet 套件。設計上比照 .NET 的 ServiceCollection，多做了一個 SubscriptionCollection 管理訂閱者，並以反射自動找出所有實作 IEventHandler\u003CT> 的類別；同一組 API 可在記憶體與 RabbitMQ（EasyNetQ）之間直接切換，並支援以 Attribute 宣告事件的 Exchange、優先權與版本。",2023,"DKcPpcdg6fck-9W2Qojmu1yLd4f2mFi76ud0fFRf56s",1792959149968]