[{"data":1,"prerenderedAt":3249},["ShallowReactive",2],{"page-\u002Farchitecture\u002F初识cleanarchitecture架构项目":3},{"id":4,"title":5,"body":6,"description":12,"extension":3237,"meta":3238,"navigation":57,"path":3245,"seo":3246,"stem":3247,"__hash__":3248},"content\u002Farchitecture\u002F初识CleanArchitecture架构项目.md","初识CleanArchitecture架构项目",{"type":7,"value":8,"toc":3225},"minimark",[9,13,16,77,83,89,908,914,920,924,927,930,933,936,939,942,950,1102,1106,1109,1174,1177,1278,1318,1341,1344,1347,1441,1444,1854,1857,1892,1895,1898,1952,2012,2028,2031,2043,2046,2095,2098,2113,2144,2147,2166,2173,2302,2316,2623,2637,2642,2673,2680,2683,2686,2689,2692,2831,2834,3011,3014,3034,3037,3043,3188,3191,3194,3197,3201,3204,3207,3211,3214,3218,3221],[10,11,12],"p",{},"这个是个非常典型的基于DDD实践的，和基于Genocs工具包实现的一个模板文件，我们·可以基于命令简单的通过模板创建项目和部署。",[10,14,15],{},"我们可以在终端里用dotnet命令安装模板和指定版本：",[17,18,23],"pre",{"className":19,"code":20,"language":21,"meta":22,"style":22},"language-shell shiki shiki-themes github-light github-dark","# .NET 7\u002F8\u002F9\u002F10+ 推荐命令\ndotnet new install Genocs.CleanArchitecture.Template\n\n# 如需安装指定版本（可选）\ndotnet new install Genocs.CleanArchitecture.Template::9.0.0\n","shell","",[24,25,26,35,52,59,65],"code",{"__ignoreMap":22},[27,28,31],"span",{"class":29,"line":30},"line",1,[27,32,34],{"class":33},"sJ8bj","# .NET 7\u002F8\u002F9\u002F10+ 推荐命令\n",[27,36,38,42,46,49],{"class":29,"line":37},2,[27,39,41],{"class":40},"sScJk","dotnet",[27,43,45],{"class":44},"sZZnC"," new",[27,47,48],{"class":44}," install",[27,50,51],{"class":44}," Genocs.CleanArchitecture.Template\n",[27,53,55],{"class":29,"line":54},3,[27,56,58],{"emptyLinePlaceholder":57},true,"\n",[27,60,62],{"class":29,"line":61},4,[27,63,64],{"class":33},"# 如需安装指定版本（可选）\n",[27,66,68,70,72,74],{"class":29,"line":67},5,[27,69,41],{"class":40},[27,71,45],{"class":44},[27,73,48],{"class":44},[27,75,76],{"class":44}," Genocs.CleanArchitecture.Template::9.0.0\n",[78,79,80],"blockquote",{},[10,81,82],{},"注：在较旧的 .NET 6 及更早版本中，安装语法为 dotnet new -i Genocs.CleanArchitecture.Template。",[10,84,85,86],{},"模板对应的短名称（Short Name）为 gnx-cleanarchitecture，可以通过 --help查看支持的全部配置项（如支持的数据库、消息代理选型等）：\n",[24,87,88],{},"dotnet new gnx-cleanarchitecture --help",[17,90,92],{"className":19,"code":91,"language":21,"meta":22,"style":22},"[17:27:11] PS C:\\Users\\20742>  dotnet new gnx-cleanarchitecture --help\nGenocs: Clean Architecture Template (C#)\nAuthor: Nocco Giovanni Emanuele\n\nUsage:\n  dotnet new gnx-cleanarchitecture [options] [template options]\n\nOptions:\n  -n, --name \u003Cname>       The name for the output being created. If no name is specified, the name of the output\n                          directory is used.\n  -o, --output \u003Coutput>   Location to place the generated output.\n  --dry-run               Displays a summary of what would happen if the given command line were run if it would result\n                          in a template creation. [default: False]\n  --force                 Forces content to be generated even if it would change existing files. [default: False]\n  --no-update-check       Disables checking for the template package updates when instantiating a template. [default:\n                          False]\n  --project \u003Cproject>     The project that should be used for context evaluation.\n  -lang, --language \u003CC#>  Specifies the template language to instantiate.\n  --type \u003Csolution>       Specifies the template type to instantiate.\n\nTemplate options:\n  -uc, --use-cases \u003Cbasic|full|readonly>   Type: choice\n                                             full      Full Wallet Account Balance Management (Read\u002FWrite)\n                                             basic     Allow Customer to Register and to Get Customer Details\n                                           (Read\u002FWrite)\n                                             readonly  Read Customer and Account Details (Read Only)\n                                           Default: full\n  -sb, --service-bus \u003Cchoice>              Type: choice\n                                             azureservicebus  Azure Service Bus (AzureServiceBus)\n                                             masstransit      Mass Transit Service Bus (MassTransit)\n                                             nservicebus      Particular Software Service Bus (NServiceBus)\n                                             rebus            Rebus Service Bus (Rebus)\n                                           Default: rebus\n  -da, --database \u003Cmongodb|sqlserver|...>  Type: choice\n                                             mongodb    MongoDB Database (MongoDb)\n                                             sqlserver  SQL Server Database (SQLServer)\n                                             inmemory   In Memory database for Debug (InMemory)\n                                           Default: sqlserver\n\n[17:27:14] PS C:\\Users\\20742>\n",[24,93,94,117,134,148,152,157,171,176,182,250,261,297,353,359,402,441,447,487,521,551,556,565,599,623,653,665,682,691,714,732,751,770,786,794,825,840,857,880,888,893],{"__ignoreMap":22},[27,95,96,100,104,107,110,114],{"class":29,"line":30},[27,97,99],{"class":98},"sVt8B","[17:27:11] PS C:",[27,101,103],{"class":102},"sj4cs","\\U",[27,105,106],{"class":98},"sers",[27,108,109],{"class":102},"\\2",[27,111,113],{"class":112},"szBVR","0742>",[27,115,116],{"class":98},"  dotnet new gnx-cleanarchitecture --help\n",[27,118,119,122,125,128,131],{"class":29,"line":37},[27,120,121],{"class":40},"Genocs:",[27,123,124],{"class":44}," Clean",[27,126,127],{"class":44}," Architecture",[27,129,130],{"class":44}," Template",[27,132,133],{"class":98}," (C#)\n",[27,135,136,139,142,145],{"class":29,"line":54},[27,137,138],{"class":40},"Author:",[27,140,141],{"class":44}," Nocco",[27,143,144],{"class":44}," Giovanni",[27,146,147],{"class":44}," Emanuele\n",[27,149,150],{"class":29,"line":61},[27,151,58],{"emptyLinePlaceholder":57},[27,153,154],{"class":29,"line":67},[27,155,156],{"class":40},"Usage:\n",[27,158,160,163,165,168],{"class":29,"line":159},6,[27,161,162],{"class":40},"  dotnet",[27,164,45],{"class":44},[27,166,167],{"class":44}," gnx-cleanarchitecture",[27,169,170],{"class":98}," [options] [template options]\n",[27,172,174],{"class":29,"line":173},7,[27,175,58],{"emptyLinePlaceholder":57},[27,177,179],{"class":29,"line":178},8,[27,180,181],{"class":40},"Options:\n",[27,183,185,188,191,194,197,200,203,206,209,212,215,218,221,224,227,230,232,235,238,240,242,245,247],{"class":29,"line":184},9,[27,186,187],{"class":40},"  -n,",[27,189,190],{"class":102}," --name",[27,192,193],{"class":112}," \u003C",[27,195,196],{"class":44},"nam",[27,198,199],{"class":98},"e",[27,201,202],{"class":112},">",[27,204,205],{"class":44},"       The",[27,207,208],{"class":44}," name",[27,210,211],{"class":44}," for",[27,213,214],{"class":44}," the",[27,216,217],{"class":44}," output",[27,219,220],{"class":44}," being",[27,222,223],{"class":44}," created.",[27,225,226],{"class":44}," If",[27,228,229],{"class":44}," no",[27,231,208],{"class":44},[27,233,234],{"class":44}," is",[27,236,237],{"class":44}," specified,",[27,239,214],{"class":44},[27,241,208],{"class":44},[27,243,244],{"class":44}," of",[27,246,214],{"class":44},[27,248,249],{"class":44}," output\n",[27,251,253,256,258],{"class":29,"line":252},10,[27,254,255],{"class":40},"                          directory",[27,257,234],{"class":44},[27,259,260],{"class":44}," used.\n",[27,262,264,267,270,272,275,278,280,283,286,289,291,294],{"class":29,"line":263},11,[27,265,266],{"class":40},"  -o,",[27,268,269],{"class":102}," --output",[27,271,193],{"class":112},[27,273,274],{"class":44},"outpu",[27,276,277],{"class":98},"t",[27,279,202],{"class":112},[27,281,282],{"class":44},"   Location",[27,284,285],{"class":44}," to",[27,287,288],{"class":44}," place",[27,290,214],{"class":44},[27,292,293],{"class":44}," generated",[27,295,296],{"class":44}," output.\n",[27,298,300,303,306,309,312,314,317,320,323,326,328,331,334,337,340,343,345,348,350],{"class":29,"line":299},12,[27,301,302],{"class":40},"  --dry-run",[27,304,305],{"class":44},"               Displays",[27,307,308],{"class":44}," a",[27,310,311],{"class":44}," summary",[27,313,244],{"class":44},[27,315,316],{"class":44}," what",[27,318,319],{"class":44}," would",[27,321,322],{"class":44}," happen",[27,324,325],{"class":44}," if",[27,327,214],{"class":44},[27,329,330],{"class":44}," given",[27,332,333],{"class":44}," command",[27,335,336],{"class":44}," line",[27,338,339],{"class":44}," were",[27,341,342],{"class":44}," run",[27,344,325],{"class":44},[27,346,347],{"class":44}," it",[27,349,319],{"class":44},[27,351,352],{"class":44}," result\n",[27,354,356],{"class":29,"line":355},13,[27,357,358],{"class":98},"                          in a template creation. [default: False]\n",[27,360,362,365,368,371,373,376,378,381,383,385,387,390,393,396,399],{"class":29,"line":361},14,[27,363,364],{"class":40},"  --force",[27,366,367],{"class":44},"                 Forces",[27,369,370],{"class":44}," content",[27,372,285],{"class":44},[27,374,375],{"class":44}," be",[27,377,293],{"class":44},[27,379,380],{"class":44}," even",[27,382,325],{"class":44},[27,384,347],{"class":44},[27,386,319],{"class":44},[27,388,389],{"class":44}," change",[27,391,392],{"class":44}," existing",[27,394,395],{"class":44}," files.",[27,397,398],{"class":98}," [default: ",[27,400,401],{"class":44},"False]\n",[27,403,405,408,411,414,416,418,421,424,427,430,433,435,438],{"class":29,"line":404},15,[27,406,407],{"class":40},"  --no-update-check",[27,409,410],{"class":44},"       Disables",[27,412,413],{"class":44}," checking",[27,415,211],{"class":44},[27,417,214],{"class":44},[27,419,420],{"class":44}," template",[27,422,423],{"class":44}," package",[27,425,426],{"class":44}," updates",[27,428,429],{"class":44}," when",[27,431,432],{"class":44}," instantiating",[27,434,308],{"class":44},[27,436,437],{"class":44}," template.",[27,439,440],{"class":98}," [default:\n",[27,442,444],{"class":29,"line":443},16,[27,445,446],{"class":40},"                          False]\n",[27,448,450,453,455,458,460,462,465,468,471,474,476,479,481,484],{"class":29,"line":449},17,[27,451,452],{"class":40},"  --project",[27,454,193],{"class":112},[27,456,457],{"class":44},"projec",[27,459,277],{"class":98},[27,461,202],{"class":112},[27,463,464],{"class":44},"     The",[27,466,467],{"class":44}," project",[27,469,470],{"class":44}," that",[27,472,473],{"class":44}," should",[27,475,375],{"class":44},[27,477,478],{"class":44}," used",[27,480,211],{"class":44},[27,482,483],{"class":44}," context",[27,485,486],{"class":44}," evaluation.\n",[27,488,490,493,496,498,501,504,506,509,511,513,516,518],{"class":29,"line":489},18,[27,491,492],{"class":40},"  -lang,",[27,494,495],{"class":102}," --language",[27,497,193],{"class":112},[27,499,500],{"class":44},"C",[27,502,503],{"class":98},"#",[27,505,202],{"class":112},[27,507,508],{"class":44},"  Specifies",[27,510,214],{"class":44},[27,512,420],{"class":44},[27,514,515],{"class":44}," language",[27,517,285],{"class":44},[27,519,520],{"class":44}," instantiate.\n",[27,522,524,527,529,532,535,537,540,542,544,547,549],{"class":29,"line":523},19,[27,525,526],{"class":40},"  --type",[27,528,193],{"class":112},[27,530,531],{"class":44},"solutio",[27,533,534],{"class":98},"n",[27,536,202],{"class":112},[27,538,539],{"class":44},"       Specifies",[27,541,214],{"class":44},[27,543,420],{"class":44},[27,545,546],{"class":44}," type",[27,548,285],{"class":44},[27,550,520],{"class":44},[27,552,554],{"class":29,"line":553},20,[27,555,58],{"emptyLinePlaceholder":57},[27,557,559,562],{"class":29,"line":558},21,[27,560,561],{"class":40},"Template",[27,563,564],{"class":44}," options:\n",[27,566,568,571,574,576,579,582,585,587,590,593,596],{"class":29,"line":567},22,[27,569,570],{"class":40},"  -uc,",[27,572,573],{"class":102}," --use-cases",[27,575,193],{"class":112},[27,577,578],{"class":44},"basic",[27,580,581],{"class":112},"|",[27,583,584],{"class":40},"full",[27,586,581],{"class":112},[27,588,589],{"class":40},"readonly",[27,591,592],{"class":98},">   ",[27,594,595],{"class":44},"Type:",[27,597,598],{"class":44}," choice\n",[27,600,602,605,608,611,614,617,620],{"class":29,"line":601},23,[27,603,604],{"class":40},"                                             full",[27,606,607],{"class":44},"      Full",[27,609,610],{"class":44}," Wallet",[27,612,613],{"class":44}," Account",[27,615,616],{"class":44}," Balance",[27,618,619],{"class":44}," Management",[27,621,622],{"class":98}," (Read\u002FWrite)\n",[27,624,626,629,632,635,637,640,643,645,648,650],{"class":29,"line":625},24,[27,627,628],{"class":40},"                                             basic",[27,630,631],{"class":44},"     Allow",[27,633,634],{"class":44}," Customer",[27,636,285],{"class":44},[27,638,639],{"class":44}," Register",[27,641,642],{"class":44}," and",[27,644,285],{"class":44},[27,646,647],{"class":44}," Get",[27,649,634],{"class":44},[27,651,652],{"class":44}," Details\n",[27,654,656,659,662],{"class":29,"line":655},25,[27,657,658],{"class":98},"                                           (",[27,660,661],{"class":40},"Read\u002FWrite",[27,663,664],{"class":98},")\n",[27,666,668,671,674,677,680],{"class":29,"line":667},26,[27,669,670],{"class":112},"                                             readonly",[27,672,673],{"class":98},"  Read Customer and Account Details (",[27,675,676],{"class":40},"Read",[27,678,679],{"class":44}," Only",[27,681,664],{"class":98},[27,683,685,688],{"class":29,"line":684},27,[27,686,687],{"class":40},"                                           Default:",[27,689,690],{"class":44}," full\n",[27,692,694,697,700,702,705,707,709,712],{"class":29,"line":693},28,[27,695,696],{"class":40},"  -sb,",[27,698,699],{"class":102}," --service-bus",[27,701,193],{"class":112},[27,703,704],{"class":44},"choic",[27,706,199],{"class":98},[27,708,202],{"class":112},[27,710,711],{"class":44},"              Type:",[27,713,598],{"class":44},[27,715,717,720,723,726,729],{"class":29,"line":716},29,[27,718,719],{"class":40},"                                             azureservicebus",[27,721,722],{"class":44},"  Azure",[27,724,725],{"class":44}," Service",[27,727,728],{"class":44}," Bus",[27,730,731],{"class":98}," (AzureServiceBus)\n",[27,733,735,738,741,744,746,748],{"class":29,"line":734},30,[27,736,737],{"class":40},"                                             masstransit",[27,739,740],{"class":44},"      Mass",[27,742,743],{"class":44}," Transit",[27,745,725],{"class":44},[27,747,728],{"class":44},[27,749,750],{"class":98}," (MassTransit)\n",[27,752,754,757,760,763,765,767],{"class":29,"line":753},31,[27,755,756],{"class":40},"                                             nservicebus",[27,758,759],{"class":44},"      Particular",[27,761,762],{"class":44}," Software",[27,764,725],{"class":44},[27,766,728],{"class":44},[27,768,769],{"class":98}," (NServiceBus)\n",[27,771,773,776,779,781,783],{"class":29,"line":772},32,[27,774,775],{"class":40},"                                             rebus",[27,777,778],{"class":44},"            Rebus",[27,780,725],{"class":44},[27,782,728],{"class":44},[27,784,785],{"class":98}," (Rebus)\n",[27,787,789,791],{"class":29,"line":788},33,[27,790,687],{"class":40},[27,792,793],{"class":44}," rebus\n",[27,795,797,800,803,805,808,810,813,815,818,821,823],{"class":29,"line":796},34,[27,798,799],{"class":40},"  -da,",[27,801,802],{"class":102}," --database",[27,804,193],{"class":112},[27,806,807],{"class":44},"mongodb",[27,809,581],{"class":112},[27,811,812],{"class":40},"sqlserver",[27,814,581],{"class":112},[27,816,817],{"class":102},"...",[27,819,820],{"class":98},">  ",[27,822,595],{"class":44},[27,824,598],{"class":44},[27,826,828,831,834,837],{"class":29,"line":827},35,[27,829,830],{"class":40},"                                             mongodb",[27,832,833],{"class":44},"    MongoDB",[27,835,836],{"class":44}," Database",[27,838,839],{"class":98}," (MongoDb)\n",[27,841,843,846,849,852,854],{"class":29,"line":842},36,[27,844,845],{"class":40},"                                             sqlserver",[27,847,848],{"class":44},"  SQL",[27,850,851],{"class":44}," Server",[27,853,836],{"class":44},[27,855,856],{"class":98}," (SQLServer)\n",[27,858,860,863,866,869,872,874,877],{"class":29,"line":859},37,[27,861,862],{"class":40},"                                             inmemory",[27,864,865],{"class":44},"   In",[27,867,868],{"class":44}," Memory",[27,870,871],{"class":44}," database",[27,873,211],{"class":44},[27,875,876],{"class":44}," Debug",[27,878,879],{"class":98}," (InMemory)\n",[27,881,883,885],{"class":29,"line":882},38,[27,884,687],{"class":40},[27,886,887],{"class":44}," sqlserver\n",[27,889,891],{"class":29,"line":890},39,[27,892,58],{"emptyLinePlaceholder":57},[27,894,896,899,901,903,905],{"class":29,"line":895},40,[27,897,898],{"class":98},"[17:27:14] PS C:",[27,900,103],{"class":102},[27,902,106],{"class":98},[27,904,109],{"class":102},[27,906,907],{"class":112},"0742>\n",[10,909,910,911],{},"创建：",[24,912,913],{},"dotnet new gnx-cleanarchitecture --name \"dddotNet\" -o \".\u002FdddotNet\"",[10,915,916,917],{},"更新： ",[24,918,919],{},"dotnet new install Genocs.CleanArchitecture.Template",[921,922,923],"h2",{"id":923},"工具链",[10,925,926],{},"Genocs 系列是该模板的基石组件，提供了统一的基础设施抽象、规范与工具链：",[10,928,929],{},"DDD的基础模型包括实体、聚合根、通用拓展与类型定义、结构化日志封装与统一配置比如Domain、Contracts文件夹里的内容由Gencos.Common工具提供。结构化日志封装与统一配置比如webapi、Worker由Gencos.Logging提供。OpenTelemetry \u002F APM 链路追踪与指标收集由Genocs.Telemetry提供。OpenAPI \u002F Swagger 接口文档自动化配置与增强比如webapi里的内容由Genocs.WebApi.OpenApi提供。",[921,931,932],{"id":932},"架构分层",[10,934,935],{},"数据持久化实现的位置默认在Infrastructure.csproj与 Host.csproj中：",[10,937,938],{},"• Microsoft.EntityFrameworkCore.SqlServer (10.0.8)：SQL Server 数据库持久化提供程序。\n• Microsoft.EntityFrameworkCore.InMemory (10.0.8)：内存数据库（常用于本地快速开发或调试）。\n• Microsoft.EntityFrameworkCore.Tools \u002F Design (10.0.8)：EF Core 迁移与脚手架工具。",[10,940,941],{},"传统的架构中，持久层是直接把实体类与数据库表绑定，一个模型打到底，但在本项目中，持久化相关的职责被精细地切分在不同层级中：",[17,943,948],{"className":944,"code":946,"language":947},[945],"language-text","│ Diagram exceeds terminal width (393 > 119 cols)\n│ Displayed as code block. Widen terminal to view inline.\n","text",[24,949,946],{"__ignoreMap":22},[17,951,955],{"className":952,"code":953,"language":954,"meta":22,"style":22},"language-mermaid shiki shiki-themes github-light github-dark","flowchart TD\n        subgraph Domain[\"1. 领域层 (Domain)\"]\n            IAccount[\"纯领域模型\u002F接口\u003Cbr\u002F>(IAccount, Customer, Money)\"]\n            IEntityFactory[\"领域工厂抽象\u003Cbr\u002F>(IEntityFactory)\"]\n        end\n\n        subgraph Application[\"2. 应用层 (Application)\"]\n            UseCase[\"业务用例 (Deposit, Register)\"]\n            IRepo[\"仓储契约 (IAccountRepository)\"]\n            IUoW[\"工作单元契约 (IUnitOfWork)\"]\n        end\n\n        subgraph Infrastructure[\"3. 基础设施层 (Infrastructure.PersistenceLayer.SQLServer)\"]\n            Context[\"EF Core 上下文 (GenocsContext)\"]\n            PersistEntity[\"持久化专用实体 (Account 继承自 Domain.Account)\"]\n            EntityFactory[\"实体工厂实现 (EntityFactory)\"]\n            RepoImpl[\"仓储实现 (AccountRepository)\"]\n            UoWImpl[\"工作单元实现 (UnitOfWork)\"]\n            Migrations[\"数据库迁移 (Migrations)\"]\n        end\n\n        UseCase --> IRepo\n        UseCase --> IUoW\n        UseCase --> IEntityFactory\n        RepoImpl -.->|实现| IRepo\n        UoWImpl -.->|实现| IUoW\n        EntityFactory -.->|实现| IEntityFactory\n        PersistEntity -.->|派生| IAccount\n        RepoImpl --> Context\n        UoWImpl --> Context\n","mermaid",[24,956,957,962,967,972,977,982,986,991,996,1001,1006,1010,1014,1019,1024,1029,1034,1039,1044,1049,1053,1057,1062,1067,1072,1077,1082,1087,1092,1097],{"__ignoreMap":22},[27,958,959],{"class":29,"line":30},[27,960,961],{},"flowchart TD\n",[27,963,964],{"class":29,"line":37},[27,965,966],{},"        subgraph Domain[\"1. 领域层 (Domain)\"]\n",[27,968,969],{"class":29,"line":54},[27,970,971],{},"            IAccount[\"纯领域模型\u002F接口\u003Cbr\u002F>(IAccount, Customer, Money)\"]\n",[27,973,974],{"class":29,"line":61},[27,975,976],{},"            IEntityFactory[\"领域工厂抽象\u003Cbr\u002F>(IEntityFactory)\"]\n",[27,978,979],{"class":29,"line":67},[27,980,981],{},"        end\n",[27,983,984],{"class":29,"line":159},[27,985,58],{"emptyLinePlaceholder":57},[27,987,988],{"class":29,"line":173},[27,989,990],{},"        subgraph Application[\"2. 应用层 (Application)\"]\n",[27,992,993],{"class":29,"line":178},[27,994,995],{},"            UseCase[\"业务用例 (Deposit, Register)\"]\n",[27,997,998],{"class":29,"line":184},[27,999,1000],{},"            IRepo[\"仓储契约 (IAccountRepository)\"]\n",[27,1002,1003],{"class":29,"line":252},[27,1004,1005],{},"            IUoW[\"工作单元契约 (IUnitOfWork)\"]\n",[27,1007,1008],{"class":29,"line":263},[27,1009,981],{},[27,1011,1012],{"class":29,"line":299},[27,1013,58],{"emptyLinePlaceholder":57},[27,1015,1016],{"class":29,"line":355},[27,1017,1018],{},"        subgraph Infrastructure[\"3. 基础设施层 (Infrastructure.PersistenceLayer.SQLServer)\"]\n",[27,1020,1021],{"class":29,"line":361},[27,1022,1023],{},"            Context[\"EF Core 上下文 (GenocsContext)\"]\n",[27,1025,1026],{"class":29,"line":404},[27,1027,1028],{},"            PersistEntity[\"持久化专用实体 (Account 继承自 Domain.Account)\"]\n",[27,1030,1031],{"class":29,"line":443},[27,1032,1033],{},"            EntityFactory[\"实体工厂实现 (EntityFactory)\"]\n",[27,1035,1036],{"class":29,"line":449},[27,1037,1038],{},"            RepoImpl[\"仓储实现 (AccountRepository)\"]\n",[27,1040,1041],{"class":29,"line":489},[27,1042,1043],{},"            UoWImpl[\"工作单元实现 (UnitOfWork)\"]\n",[27,1045,1046],{"class":29,"line":523},[27,1047,1048],{},"            Migrations[\"数据库迁移 (Migrations)\"]\n",[27,1050,1051],{"class":29,"line":553},[27,1052,981],{},[27,1054,1055],{"class":29,"line":558},[27,1056,58],{"emptyLinePlaceholder":57},[27,1058,1059],{"class":29,"line":567},[27,1060,1061],{},"        UseCase --> IRepo\n",[27,1063,1064],{"class":29,"line":601},[27,1065,1066],{},"        UseCase --> IUoW\n",[27,1068,1069],{"class":29,"line":625},[27,1070,1071],{},"        UseCase --> IEntityFactory\n",[27,1073,1074],{"class":29,"line":655},[27,1075,1076],{},"        RepoImpl -.->|实现| IRepo\n",[27,1078,1079],{"class":29,"line":667},[27,1080,1081],{},"        UoWImpl -.->|实现| IUoW\n",[27,1083,1084],{"class":29,"line":684},[27,1085,1086],{},"        EntityFactory -.->|实现| IEntityFactory\n",[27,1088,1089],{"class":29,"line":693},[27,1090,1091],{},"        PersistEntity -.->|派生| IAccount\n",[27,1093,1094],{"class":29,"line":716},[27,1095,1096],{},"        RepoImpl --> Context\n",[27,1098,1099],{"class":29,"line":734},[27,1100,1101],{},"        UoWImpl --> Context\n",[1103,1104,1105],"h3",{"id":1105},"领域模型层",[10,1107,1108],{},"契约定义层（位于 Domain 与 Application）,比如Application\u002FRepositories的IAccountRespository.cs中的：",[17,1110,1114],{"className":1111,"code":1112,"language":1113,"meta":22,"style":22},"language-cs shiki shiki-themes github-light github-dark","using dddotNet.Domain.Accounts;\n\nnamespace dddotNet.Application.Repositories;\n\npublic interface IAccountRepository\n{\n    Task\u003CIAccount?> GetAsync(Guid id, CancellationToken cancellationToken = default);\n    Task AddAsync(IAccount account, ICredit credit, CancellationToken cancellationToken = default);\n    Task UpdateAsync(IAccount account, ICredit credit, CancellationToken cancellationToken = default);\n    Task UpdateAsync(IAccount account, IDebit debit, CancellationToken cancellationToken = default);\n    Task DeleteAsync(IAccount account, CancellationToken cancellationToken = default);\n}\n","cs",[24,1115,1116,1121,1125,1130,1134,1139,1144,1149,1154,1159,1164,1169],{"__ignoreMap":22},[27,1117,1118],{"class":29,"line":30},[27,1119,1120],{},"using dddotNet.Domain.Accounts;\n",[27,1122,1123],{"class":29,"line":37},[27,1124,58],{"emptyLinePlaceholder":57},[27,1126,1127],{"class":29,"line":54},[27,1128,1129],{},"namespace dddotNet.Application.Repositories;\n",[27,1131,1132],{"class":29,"line":61},[27,1133,58],{"emptyLinePlaceholder":57},[27,1135,1136],{"class":29,"line":67},[27,1137,1138],{},"public interface IAccountRepository\n",[27,1140,1141],{"class":29,"line":159},[27,1142,1143],{},"{\n",[27,1145,1146],{"class":29,"line":173},[27,1147,1148],{},"    Task\u003CIAccount?> GetAsync(Guid id, CancellationToken cancellationToken = default);\n",[27,1150,1151],{"class":29,"line":178},[27,1152,1153],{},"    Task AddAsync(IAccount account, ICredit credit, CancellationToken cancellationToken = default);\n",[27,1155,1156],{"class":29,"line":184},[27,1157,1158],{},"    Task UpdateAsync(IAccount account, ICredit credit, CancellationToken cancellationToken = default);\n",[27,1160,1161],{"class":29,"line":252},[27,1162,1163],{},"    Task UpdateAsync(IAccount account, IDebit debit, CancellationToken cancellationToken = default);\n",[27,1165,1166],{"class":29,"line":263},[27,1167,1168],{},"    Task DeleteAsync(IAccount account, CancellationToken cancellationToken = default);\n",[27,1170,1171],{"class":29,"line":299},[27,1172,1173],{},"}\n",[10,1175,1176],{},"模板里订阅契约的时候直接使用的是Domain层的领域模型定义的聚合根里的内容，IEntityFactory 负责解耦领域对象与持久化对象的实例化。·",[17,1178,1180],{"className":1111,"code":1179,"language":1113,"meta":22,"style":22},"public interface IAccount : IAggregateRoot\u003CGuid>\n{\n    ICredit Deposit(IEntityFactory entityFactory, PositiveMoney amountToDeposit);\n    IDebit? Withdraw(IEntityFactory entityFactory, PositiveMoney amountToWithdraw);\n    bool IsClosingAllowed();\n    Money GetCurrentBalance();\n}\n...\n\u002F\u002F\u002F \u003Csummary>\n\u002F\u002F\u002F The \u003Cc>IEntityFactory\u003C\u002Fc> interface defines a contract for creating instances of domain entities such as customers, accounts, credits, and debits.\n\u002F\u002F\u002F It provides methods for instantiating these entities with the necessary parameters,\n\u002F\u002F\u002F ensuring that the creation logic is centralized and consistent across the application.\n\u002F\u002F\u002F \u003C\u002Fsummary>\npublic interface IEntityFactory\n{\n    ICustomer NewCustomer(SSN ssn, Name name);\n    IAccount NewAccount(ICustomer customer);\n    ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate);\n    IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate);\n}\n",[24,1181,1182,1187,1191,1196,1201,1206,1211,1215,1220,1225,1230,1235,1240,1245,1250,1254,1259,1264,1269,1274],{"__ignoreMap":22},[27,1183,1184],{"class":29,"line":30},[27,1185,1186],{},"public interface IAccount : IAggregateRoot\u003CGuid>\n",[27,1188,1189],{"class":29,"line":37},[27,1190,1143],{},[27,1192,1193],{"class":29,"line":54},[27,1194,1195],{},"    ICredit Deposit(IEntityFactory entityFactory, PositiveMoney amountToDeposit);\n",[27,1197,1198],{"class":29,"line":61},[27,1199,1200],{},"    IDebit? Withdraw(IEntityFactory entityFactory, PositiveMoney amountToWithdraw);\n",[27,1202,1203],{"class":29,"line":67},[27,1204,1205],{},"    bool IsClosingAllowed();\n",[27,1207,1208],{"class":29,"line":159},[27,1209,1210],{},"    Money GetCurrentBalance();\n",[27,1212,1213],{"class":29,"line":173},[27,1214,1173],{},[27,1216,1217],{"class":29,"line":178},[27,1218,1219],{},"...\n",[27,1221,1222],{"class":29,"line":184},[27,1223,1224],{},"\u002F\u002F\u002F \u003Csummary>\n",[27,1226,1227],{"class":29,"line":252},[27,1228,1229],{},"\u002F\u002F\u002F The \u003Cc>IEntityFactory\u003C\u002Fc> interface defines a contract for creating instances of domain entities such as customers, accounts, credits, and debits.\n",[27,1231,1232],{"class":29,"line":263},[27,1233,1234],{},"\u002F\u002F\u002F It provides methods for instantiating these entities with the necessary parameters,\n",[27,1236,1237],{"class":29,"line":299},[27,1238,1239],{},"\u002F\u002F\u002F ensuring that the creation logic is centralized and consistent across the application.\n",[27,1241,1242],{"class":29,"line":355},[27,1243,1244],{},"\u002F\u002F\u002F \u003C\u002Fsummary>\n",[27,1246,1247],{"class":29,"line":361},[27,1248,1249],{},"public interface IEntityFactory\n",[27,1251,1252],{"class":29,"line":404},[27,1253,1143],{},[27,1255,1256],{"class":29,"line":443},[27,1257,1258],{},"    ICustomer NewCustomer(SSN ssn, Name name);\n",[27,1260,1261],{"class":29,"line":449},[27,1262,1263],{},"    IAccount NewAccount(ICustomer customer);\n",[27,1265,1266],{"class":29,"line":489},[27,1267,1268],{},"    ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate);\n",[27,1270,1271],{"class":29,"line":523},[27,1272,1273],{},"    IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate);\n",[27,1275,1276],{"class":29,"line":553},[27,1277,1173],{},[10,1279,1280,1281,1284,1285,1288,1289,1292,1293,1296,1297,1300,1301,1304,1305,1300,1308,1311,1312,1314,1315,1317],{},"DDD里，领域层Domain必须保存纯粹，不能依赖具体的ORM，如Entity Frame Core、Dapper 或者特定数据库生成的类，如果 ",[24,1282,1283],{},"IAccount"," 内部直接 ",[24,1286,1287],{},"new Credit(...)"," 或 ",[24,1290,1291],{},"new Debit(...)","，甚至直接调用持久化框架的工厂\u002F上下文，那么",[24,1294,1295],{},"Account"," 必须知道 ",[24,1298,1299],{},"Credit"," \u002F ",[24,1302,1303],{},"Debit"," 的具体实现类（而不是接口 ",[24,1306,1307],{},"ICredit",[24,1309,1310],{},"IDebit","）。如果 ",[24,1313,1299],{}," 对象的创建过程需要依赖数据库主键生成器、全局 ID 生成算法、或是特定 ORM 跟踪的上下文，",[24,1316,1295],{}," 实体就会强耦合这些底层基础设施。",[10,1319,1320,1321,1324,1325,1327,1328,1331,1332,1334,1335,1337,1338,1340],{},"引入 ",[24,1322,1323],{},"IEntityFactory"," 后的解耦：",[24,1326,1295],{}," 只需要调用 ",[24,1329,1330],{},"entityFactory.CreateCredit(...)"," 或类似的抽象方法。",[24,1333,1295],{}," 只依赖 ",[24,1336,1323],{}," 抽象接口，而不关心 ",[24,1339,1299],{}," 底层到底是用什么具体类初始化的、主键是如何生成的、或者是如何注册到 ORM 跟踪上下文里的。",[10,1342,1343],{},"这个实体工厂接口的实现在SQLServer的持久层，就是这样，领域层的 Domain.Accounts.Account 只有业务逻辑和流水集合，它根本不知道关系型数据库里需要 CustomerId这个外键字段。",[1103,1345,1346],{"id":1346},"持久化的实现层",[17,1348,1350],{"className":1111,"code":1349,"language":1113,"meta":22,"style":22},"namespace dddotNet.Infrastructure.PersistenceLayer.SQLServer;\n\npublic sealed class EntityFactory : IEntityFactory\n{\n    public IAccount NewAccount(ICustomer customer)\n    {\n        var account = new Account(customer);\n        return account;\n    }\n\n    public ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate)\n        => new Credit(account, amountToDeposit, transactionDate);\n\n    public ICustomer NewCustomer(SSN ssn, Name name)\n        => new Customer(ssn, name);\n\n    public IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate)\n        => new Debit(account, amountToWithdraw, transactionDate);\n}\n",[24,1351,1352,1357,1361,1366,1370,1375,1380,1385,1390,1395,1399,1404,1409,1413,1418,1423,1427,1432,1437],{"__ignoreMap":22},[27,1353,1354],{"class":29,"line":30},[27,1355,1356],{},"namespace dddotNet.Infrastructure.PersistenceLayer.SQLServer;\n",[27,1358,1359],{"class":29,"line":37},[27,1360,58],{"emptyLinePlaceholder":57},[27,1362,1363],{"class":29,"line":54},[27,1364,1365],{},"public sealed class EntityFactory : IEntityFactory\n",[27,1367,1368],{"class":29,"line":61},[27,1369,1143],{},[27,1371,1372],{"class":29,"line":67},[27,1373,1374],{},"    public IAccount NewAccount(ICustomer customer)\n",[27,1376,1377],{"class":29,"line":159},[27,1378,1379],{},"    {\n",[27,1381,1382],{"class":29,"line":173},[27,1383,1384],{},"        var account = new Account(customer);\n",[27,1386,1387],{"class":29,"line":178},[27,1388,1389],{},"        return account;\n",[27,1391,1392],{"class":29,"line":184},[27,1393,1394],{},"    }\n",[27,1396,1397],{"class":29,"line":252},[27,1398,58],{"emptyLinePlaceholder":57},[27,1400,1401],{"class":29,"line":263},[27,1402,1403],{},"    public ICredit NewCredit(IAccount account, PositiveMoney amountToDeposit, DateTime transactionDate)\n",[27,1405,1406],{"class":29,"line":299},[27,1407,1408],{},"        => new Credit(account, amountToDeposit, transactionDate);\n",[27,1410,1411],{"class":29,"line":355},[27,1412,58],{"emptyLinePlaceholder":57},[27,1414,1415],{"class":29,"line":361},[27,1416,1417],{},"    public ICustomer NewCustomer(SSN ssn, Name name)\n",[27,1419,1420],{"class":29,"line":404},[27,1421,1422],{},"        => new Customer(ssn, name);\n",[27,1424,1425],{"class":29,"line":443},[27,1426,58],{"emptyLinePlaceholder":57},[27,1428,1429],{"class":29,"line":449},[27,1430,1431],{},"    public IDebit NewDebit(IAccount account, PositiveMoney amountToWithdraw, DateTime transactionDate)\n",[27,1433,1434],{"class":29,"line":489},[27,1435,1436],{},"        => new Debit(account, amountToWithdraw, transactionDate);\n",[27,1438,1439],{"class":29,"line":523},[27,1440,1173],{},[10,1442,1443],{},"最后，webapi中，在SQLServerInfrastructureExtensions.cs中，将SQLServer的工厂实现绑定给领域接口：",[17,1445,1447],{"className":1111,"code":1446,"language":1113,"meta":22,"style":22},"namespace dddotNet.WebApi.Extensions.SQLServer;\n\npublic static class SQLServerInfrastructureExtensions\n{\n    private static readonly Lock MigrationLock = new();\n\n    public static IServiceCollection AddSQLServerPersistence(this IServiceCollection services, IConfiguration configuration)\n    {\n        services.AddScoped\u003CIEntityFactory, EntityFactory>();\n\n        services.AddDbContext\u003CGenocsContext>(options =>\n            options\n                .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))\n                .UseSqlServer(\n                    configuration.GetConnectionString(\"DefaultConnection\"),\n                    b =>\n                    {\n                        b.MigrationsAssembly(\"dddotNet.Infrastructure\");\n                        b.EnableRetryOnFailure();\n                    }));\n\n        services.AddScoped\u003CIUnitOfWork, UnitOfWork>();\n\n        services.AddScoped\u003CIAccountRepository, AccountRepository>();\n        services.AddScoped\u003CICustomerRepository, CustomerRepository>();\n\n        return services;\n    }\n\n    public static IServiceProvider UseSqlServerPersistence(this IServiceProvider serviceProvider)\n    {\n        const int maxAttempts = 5;\n\n        for (int attempt = 1; attempt \u003C= maxAttempts; attempt++)\n        {\n            try\n            {\n                using var scope = serviceProvider.CreateScope();\n                var context = scope.ServiceProvider.GetService\u003CGenocsContext>();\n\n                if (context?.Database.IsSqlServer() != true)\n                {\n                    return serviceProvider;\n                }\n\n                lock (MigrationLock)\n                {\n                    \u002F\u002F CreateExecutionStrategy retries transient failures while creating\u002Fmigrating a fresh database.\n                    var strategy = context.Database.CreateExecutionStrategy();\n                    strategy.Execute(() => context.Database.Migrate());\n                }\n\n                if (context.Database.CanConnect())\n                {\n                    return serviceProvider;\n                }\n            }\n            catch (SqlException) when (attempt \u003C maxAttempts)\n            {\n                Thread.Sleep(TimeSpan.FromMilliseconds(250 * attempt));\n            }\n        }\n\n        \u002F\u002F Last attempt should surface an actionable exception instead of silently starting with a broken DB.\n        using (var scope = serviceProvider.CreateScope())\n        {\n            var context = scope.ServiceProvider.GetService\u003CGenocsContext>();\n            if (context?.Database.IsSqlServer() == true)\n            {\n                lock (MigrationLock)\n                {\n                    var strategy = context.Database.CreateExecutionStrategy();\n                    strategy.Execute(() => context.Database.Migrate());\n                }\n            }\n        }\n\n        return serviceProvider;\n    }\n}\n",[24,1448,1449,1454,1458,1463,1467,1472,1476,1481,1485,1490,1494,1499,1504,1509,1514,1519,1524,1529,1534,1539,1544,1548,1553,1557,1562,1567,1571,1576,1580,1584,1589,1593,1598,1602,1607,1612,1617,1622,1627,1632,1636,1642,1648,1654,1660,1665,1671,1676,1682,1688,1694,1699,1704,1710,1715,1720,1725,1731,1737,1742,1748,1753,1759,1764,1770,1776,1781,1787,1793,1798,1803,1808,1813,1818,1823,1828,1833,1838,1844,1849],{"__ignoreMap":22},[27,1450,1451],{"class":29,"line":30},[27,1452,1453],{},"namespace dddotNet.WebApi.Extensions.SQLServer;\n",[27,1455,1456],{"class":29,"line":37},[27,1457,58],{"emptyLinePlaceholder":57},[27,1459,1460],{"class":29,"line":54},[27,1461,1462],{},"public static class SQLServerInfrastructureExtensions\n",[27,1464,1465],{"class":29,"line":61},[27,1466,1143],{},[27,1468,1469],{"class":29,"line":67},[27,1470,1471],{},"    private static readonly Lock MigrationLock = new();\n",[27,1473,1474],{"class":29,"line":159},[27,1475,58],{"emptyLinePlaceholder":57},[27,1477,1478],{"class":29,"line":173},[27,1479,1480],{},"    public static IServiceCollection AddSQLServerPersistence(this IServiceCollection services, IConfiguration configuration)\n",[27,1482,1483],{"class":29,"line":178},[27,1484,1379],{},[27,1486,1487],{"class":29,"line":184},[27,1488,1489],{},"        services.AddScoped\u003CIEntityFactory, EntityFactory>();\n",[27,1491,1492],{"class":29,"line":252},[27,1493,58],{"emptyLinePlaceholder":57},[27,1495,1496],{"class":29,"line":263},[27,1497,1498],{},"        services.AddDbContext\u003CGenocsContext>(options =>\n",[27,1500,1501],{"class":29,"line":299},[27,1502,1503],{},"            options\n",[27,1505,1506],{"class":29,"line":355},[27,1507,1508],{},"                .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))\n",[27,1510,1511],{"class":29,"line":361},[27,1512,1513],{},"                .UseSqlServer(\n",[27,1515,1516],{"class":29,"line":404},[27,1517,1518],{},"                    configuration.GetConnectionString(\"DefaultConnection\"),\n",[27,1520,1521],{"class":29,"line":443},[27,1522,1523],{},"                    b =>\n",[27,1525,1526],{"class":29,"line":449},[27,1527,1528],{},"                    {\n",[27,1530,1531],{"class":29,"line":489},[27,1532,1533],{},"                        b.MigrationsAssembly(\"dddotNet.Infrastructure\");\n",[27,1535,1536],{"class":29,"line":523},[27,1537,1538],{},"                        b.EnableRetryOnFailure();\n",[27,1540,1541],{"class":29,"line":553},[27,1542,1543],{},"                    }));\n",[27,1545,1546],{"class":29,"line":558},[27,1547,58],{"emptyLinePlaceholder":57},[27,1549,1550],{"class":29,"line":567},[27,1551,1552],{},"        services.AddScoped\u003CIUnitOfWork, UnitOfWork>();\n",[27,1554,1555],{"class":29,"line":601},[27,1556,58],{"emptyLinePlaceholder":57},[27,1558,1559],{"class":29,"line":625},[27,1560,1561],{},"        services.AddScoped\u003CIAccountRepository, AccountRepository>();\n",[27,1563,1564],{"class":29,"line":655},[27,1565,1566],{},"        services.AddScoped\u003CICustomerRepository, CustomerRepository>();\n",[27,1568,1569],{"class":29,"line":667},[27,1570,58],{"emptyLinePlaceholder":57},[27,1572,1573],{"class":29,"line":684},[27,1574,1575],{},"        return services;\n",[27,1577,1578],{"class":29,"line":693},[27,1579,1394],{},[27,1581,1582],{"class":29,"line":716},[27,1583,58],{"emptyLinePlaceholder":57},[27,1585,1586],{"class":29,"line":734},[27,1587,1588],{},"    public static IServiceProvider UseSqlServerPersistence(this IServiceProvider serviceProvider)\n",[27,1590,1591],{"class":29,"line":753},[27,1592,1379],{},[27,1594,1595],{"class":29,"line":772},[27,1596,1597],{},"        const int maxAttempts = 5;\n",[27,1599,1600],{"class":29,"line":788},[27,1601,58],{"emptyLinePlaceholder":57},[27,1603,1604],{"class":29,"line":796},[27,1605,1606],{},"        for (int attempt = 1; attempt \u003C= maxAttempts; attempt++)\n",[27,1608,1609],{"class":29,"line":827},[27,1610,1611],{},"        {\n",[27,1613,1614],{"class":29,"line":842},[27,1615,1616],{},"            try\n",[27,1618,1619],{"class":29,"line":859},[27,1620,1621],{},"            {\n",[27,1623,1624],{"class":29,"line":882},[27,1625,1626],{},"                using var scope = serviceProvider.CreateScope();\n",[27,1628,1629],{"class":29,"line":890},[27,1630,1631],{},"                var context = scope.ServiceProvider.GetService\u003CGenocsContext>();\n",[27,1633,1634],{"class":29,"line":895},[27,1635,58],{"emptyLinePlaceholder":57},[27,1637,1639],{"class":29,"line":1638},41,[27,1640,1641],{},"                if (context?.Database.IsSqlServer() != true)\n",[27,1643,1645],{"class":29,"line":1644},42,[27,1646,1647],{},"                {\n",[27,1649,1651],{"class":29,"line":1650},43,[27,1652,1653],{},"                    return serviceProvider;\n",[27,1655,1657],{"class":29,"line":1656},44,[27,1658,1659],{},"                }\n",[27,1661,1663],{"class":29,"line":1662},45,[27,1664,58],{"emptyLinePlaceholder":57},[27,1666,1668],{"class":29,"line":1667},46,[27,1669,1670],{},"                lock (MigrationLock)\n",[27,1672,1674],{"class":29,"line":1673},47,[27,1675,1647],{},[27,1677,1679],{"class":29,"line":1678},48,[27,1680,1681],{},"                    \u002F\u002F CreateExecutionStrategy retries transient failures while creating\u002Fmigrating a fresh database.\n",[27,1683,1685],{"class":29,"line":1684},49,[27,1686,1687],{},"                    var strategy = context.Database.CreateExecutionStrategy();\n",[27,1689,1691],{"class":29,"line":1690},50,[27,1692,1693],{},"                    strategy.Execute(() => context.Database.Migrate());\n",[27,1695,1697],{"class":29,"line":1696},51,[27,1698,1659],{},[27,1700,1702],{"class":29,"line":1701},52,[27,1703,58],{"emptyLinePlaceholder":57},[27,1705,1707],{"class":29,"line":1706},53,[27,1708,1709],{},"                if (context.Database.CanConnect())\n",[27,1711,1713],{"class":29,"line":1712},54,[27,1714,1647],{},[27,1716,1718],{"class":29,"line":1717},55,[27,1719,1653],{},[27,1721,1723],{"class":29,"line":1722},56,[27,1724,1659],{},[27,1726,1728],{"class":29,"line":1727},57,[27,1729,1730],{},"            }\n",[27,1732,1734],{"class":29,"line":1733},58,[27,1735,1736],{},"            catch (SqlException) when (attempt \u003C maxAttempts)\n",[27,1738,1740],{"class":29,"line":1739},59,[27,1741,1621],{},[27,1743,1745],{"class":29,"line":1744},60,[27,1746,1747],{},"                Thread.Sleep(TimeSpan.FromMilliseconds(250 * attempt));\n",[27,1749,1751],{"class":29,"line":1750},61,[27,1752,1730],{},[27,1754,1756],{"class":29,"line":1755},62,[27,1757,1758],{},"        }\n",[27,1760,1762],{"class":29,"line":1761},63,[27,1763,58],{"emptyLinePlaceholder":57},[27,1765,1767],{"class":29,"line":1766},64,[27,1768,1769],{},"        \u002F\u002F Last attempt should surface an actionable exception instead of silently starting with a broken DB.\n",[27,1771,1773],{"class":29,"line":1772},65,[27,1774,1775],{},"        using (var scope = serviceProvider.CreateScope())\n",[27,1777,1779],{"class":29,"line":1778},66,[27,1780,1611],{},[27,1782,1784],{"class":29,"line":1783},67,[27,1785,1786],{},"            var context = scope.ServiceProvider.GetService\u003CGenocsContext>();\n",[27,1788,1790],{"class":29,"line":1789},68,[27,1791,1792],{},"            if (context?.Database.IsSqlServer() == true)\n",[27,1794,1796],{"class":29,"line":1795},69,[27,1797,1621],{},[27,1799,1801],{"class":29,"line":1800},70,[27,1802,1670],{},[27,1804,1806],{"class":29,"line":1805},71,[27,1807,1647],{},[27,1809,1811],{"class":29,"line":1810},72,[27,1812,1687],{},[27,1814,1816],{"class":29,"line":1815},73,[27,1817,1693],{},[27,1819,1821],{"class":29,"line":1820},74,[27,1822,1659],{},[27,1824,1826],{"class":29,"line":1825},75,[27,1827,1730],{},[27,1829,1831],{"class":29,"line":1830},76,[27,1832,1758],{},[27,1834,1836],{"class":29,"line":1835},77,[27,1837,58],{"emptyLinePlaceholder":57},[27,1839,1841],{"class":29,"line":1840},78,[27,1842,1843],{},"        return serviceProvider;\n",[27,1845,1847],{"class":29,"line":1846},79,[27,1848,1394],{},[27,1850,1852],{"class":29,"line":1851},80,[27,1853,1173],{},[10,1855,1856],{},"AddSQLServerPersistence方法中，应用层的接口都被绑定到了具体的实现上：",[1858,1859,1860,1869,1883],"ul",{},[1861,1862,1863,1865,1866],"li",{},[24,1864,1323],{}," -> ",[24,1867,1868],{},"EntityFactory",[1861,1870,1871,1865,1874,1877,1878,1882],{},[24,1872,1873],{},"IUnitOfWork",[24,1875,1876],{},"UnitOfWork"," （正是我们刚才讨论的",[1879,1880,1881],"strong",{},"事务边界","实现）",[1861,1884,1885,1300,1888,1891],{},[24,1886,1887],{},"IAccountRepository",[24,1889,1890],{},"ICustomerRepository"," -> 具体实现。",[10,1893,1894],{},"从结果来说，应用层和领域层完全不知道ef core、sql server的存在，如果想更换为postgresql，只需要更新一个扩展方法，应用层完全不用修改，这个就叫做依赖倒置原则。",[10,1896,1897],{},"对于数据库的重试策略这里下了功夫：",[17,1899,1901],{"className":1111,"code":1900,"language":1113,"meta":22,"style":22},"services.AddDbContext\u003CGenocsContext>(options =>\n    options\n        .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))\n        .UseSqlServer(\n            configuration.GetConnectionString(\"DefaultConnection\"),\n            b =>\n            {\n                b.MigrationsAssembly(\"dddotNet.Infrastructure\");\n                b.EnableRetryOnFailure();\n            }));\n",[24,1902,1903,1908,1913,1918,1923,1928,1933,1937,1942,1947],{"__ignoreMap":22},[27,1904,1905],{"class":29,"line":30},[27,1906,1907],{},"services.AddDbContext\u003CGenocsContext>(options =>\n",[27,1909,1910],{"class":29,"line":37},[27,1911,1912],{},"    options\n",[27,1914,1915],{"class":29,"line":54},[27,1916,1917],{},"        .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))\n",[27,1919,1920],{"class":29,"line":61},[27,1921,1922],{},"        .UseSqlServer(\n",[27,1924,1925],{"class":29,"line":67},[27,1926,1927],{},"            configuration.GetConnectionString(\"DefaultConnection\"),\n",[27,1929,1930],{"class":29,"line":159},[27,1931,1932],{},"            b =>\n",[27,1934,1935],{"class":29,"line":173},[27,1936,1621],{},[27,1938,1939],{"class":29,"line":178},[27,1940,1941],{},"                b.MigrationsAssembly(\"dddotNet.Infrastructure\");\n",[27,1943,1944],{"class":29,"line":184},[27,1945,1946],{},"                b.EnableRetryOnFailure();\n",[27,1948,1949],{"class":29,"line":252},[27,1950,1951],{},"            }));\n",[10,1953,1954,1959,1960,1965,1966,1981,1982,1985,1986,1989,1990,1993,1994,1996,1997,2000,2001,2004,2005,2007,2008,2011],{},[1879,1955,1956],{},[24,1957,1958],{},"b.EnableRetryOnFailure()","：告诉 EF Core，如果是由于网络抖动等瞬时故障导致的 SQL 异常，自动重试。",[1879,1961,1962],{},[24,1963,1964],{},"context.Database.CreateExecutionStrategy()","：这是 EF Core 官方推荐的做法。",[1879,1967,1968,1969,1972,1973,1976,1977,1980],{},"当你开启了 ",[24,1970,1971],{},"EnableRetryOnFailure"," 后，如果手动调用 ",[24,1974,1975],{},"Migrate()"," 或者手动开启事务，必须用 ",[24,1978,1979],{},"ExecutionStrategy.Execute(...)"," 包起来","。否则 EF Core 会抛出异常。这段代码正确处理了这个细节，确保在连接不稳定时迁移也能安全重试。\nUseSqlServerPersistence里有一个防并发启动的锁机制Lock，这里用了",[24,1983,1984],{},"MigrationLock"," 锁。 这通常是为了防止在",[1879,1987,1988],{},"多线程启动","（比如同一台机器上跑多个 Worker 或者服务器）时，多个实例同时尝试执行 ",[24,1991,1992],{},"Database.Migrate()","，导致数据库表被锁死或迁移报错。\n",[24,1995,1873],{}," 注册为 ",[24,1998,1999],{},"Scoped","（每次 HTTP 请求一个）。",[1879,2002,2003],{},"在一次 HTTP 请求（即一个 Use Case 用例）中，共享同一个 UnitOfWork 和 DbContext","，从而保证事务的一致性。但要注意：如果在 ",[24,2006,1876],{}," 内部去调用第三方 API（比如发邮件、调微信支付），第三方 API 成功但 ",[24,2009,2010],{},"SaveChangesAsync"," 失败了，就会导致数据不一致。",[78,2013,2014],{},[10,2015,2016,2019,2020,2023,2024,2027],{},[1879,2017,2018],{},"建议","：在生产环境，最好不要让应用程序在启动时自动执行迁移。更规范的做法是：把数据库迁移作为一个",[1879,2021,2022],{},"独立的 CI\u002FCD 步骤","（例如使用 EF Core CLI 的 ",[24,2025,2026],{},"dotnet ef database update","），在应用部署前由流水线执行一次。",[10,2029,2030],{},"不过当前项目在 Account.Deposit(IEntityFactory, ...) 中传入工厂的设计确实“偏重”了，具有明显的过度工程（Over-engineering）痕迹。在现代 .NET \u002F DDD 工程实践中，更推荐【由聚合根内部直接创建 Credit】配合 EF Core 的高级映射能力。Genocs 模板遵循 Uncle Bob 的严苛 Clean Architecture流派）之所以这么设计，是为了追求极致的“持久化无知”",[2032,2033,2034,2037,2040],"ol",{},[1861,2035,2036],{},"它不想让 Domain 层的 Credit 包含任何数据库外键字段（如 AccountId）。",[1861,2038,2039],{},"它在基础设施层写了一个 SQLServer.Credit 继承自 Domain.Credit，专门加上了 AccountId。",[1861,2041,2042],{},"为了让领域层不知不觉用上这个带外键的派生类，它不得不发明了 IEntityFactory，并通过 Deposit 参数暗度陈仓地传进去。",[10,2044,2045],{},"而且领域模型无需声明 public Guid AccountId { get; }，EF Core 的 Fluent API 会自动在数据库层维护外键：",[17,2047,2049],{"className":1111,"code":2048,"language":1113,"meta":22,"style":22},"\u002F\u002F 在 GenocsContext.cs 中配置：\nmodelBuilder.Entity\u003CAccount>(b =>\n{\n    b.ToTable(\"Account\");\n    \u002F\u002F 配置一对多关系，外键定义为影子属性 \"AccountId\"，完全不污染领域模型\n    b.HasMany(a => a.Credits)\n     .WithOne()\n     .HasForeignKey(\"AccountId\");\n});\n",[24,2050,2051,2056,2061,2065,2070,2075,2080,2085,2090],{"__ignoreMap":22},[27,2052,2053],{"class":29,"line":30},[27,2054,2055],{},"\u002F\u002F 在 GenocsContext.cs 中配置：\n",[27,2057,2058],{"class":29,"line":37},[27,2059,2060],{},"modelBuilder.Entity\u003CAccount>(b =>\n",[27,2062,2063],{"class":29,"line":54},[27,2064,1143],{},[27,2066,2067],{"class":29,"line":61},[27,2068,2069],{},"    b.ToTable(\"Account\");\n",[27,2071,2072],{"class":29,"line":67},[27,2073,2074],{},"    \u002F\u002F 配置一对多关系，外键定义为影子属性 \"AccountId\"，完全不污染领域模型\n",[27,2076,2077],{"class":29,"line":159},[27,2078,2079],{},"    b.HasMany(a => a.Credits)\n",[27,2081,2082],{"class":29,"line":173},[27,2083,2084],{},"     .WithOne()\n",[27,2086,2087],{"class":29,"line":178},[27,2088,2089],{},"     .HasForeignKey(\"AccountId\");\n",[27,2091,2092],{"class":29,"line":184},[27,2093,2094],{},"});\n",[10,2096,2097],{},"不过用于学习的话，已经足够了。",[10,2099,2100,2101,2103,2104,2107,2108,1288,2110,2112],{},"领域层只与接口交互，具体实现被彻底隐藏在 ",[24,2102,1323],{}," 的具体实现类（如 ",[24,2105,2106],{},"SqlEntityFactory","）之后。在真实的持久化\u002F数据库体系中，新创建一条流水（",[24,2109,1299],{},[24,2111,1303],{},"）往往需要遵循严格的系统规约：",[1858,2114,2115,2130],{},[1861,2116,2117,2120,2121,2123,2124,2126,2127,2129],{},[1879,2118,2119],{},"ID 生成策略","：例如使用雪花算法（Snowflake）、UUIDv7 或者是数据库自增序列。如果直接在 ",[24,2122,1295],{}," 内部 ",[24,2125,1287],{},"，",[24,2128,1295],{}," 就不得不注入或调用 ID 生成器。",[1861,2131,2132,2135,2136,2139,2140,2143],{},[1879,2133,2134],{},"审计字段绑定","：交易时间（",[24,2137,2138],{},"transactionDate","）、创建人、版本号（用于乐观锁 ",[24,2141,2142],{},"RowVersion","）。",[10,2145,2146],{},"好了，拉回来。最上方的代码里定义的是仓储契约，定义了对业务聚合的读写规范。在ddd分层里，设计用于返回聚合根接口，保证了上层代码（如应用服务 Application Service）操作的都是抽象的领域契约，而不是具体的贫血实体或数据库实体。",[10,2148,2149,2150,2152,2153,1288,2155,2157,2158,2161,2162,2165],{},"方法签名里不仅有 ",[24,2151,1283],{},"，还带了 ",[24,2154,1307],{},[24,2156,1310],{},"，例如： ",[24,2159,2160],{},"Task UpdateAsync(IAccount account, ICredit credit, ...)"," 这说明该仓储不仅仅是更新一个账户的主表，它在底层设计上：",[1879,2163,2164],{},"“存款\u002F取款这种领域行为发生后，不仅要更新账户的余额，还要将新产生的交易流水（Credit\u002FDebit）一并原子化地持久化到数据库中”","。这保证了领域操作的完整性和事务的一致性。",[10,2167,2168,2169,2172],{},"持久化实现层（位于 Infrastructure.PersistenceLayer.SQLServer），我们可以打开 ",[24,2170,2171],{},"dddotNet.Infrastructure.PersistenceLayer.SQLServer;","内的Accout看看里面的实现：",[17,2174,2176],{"className":1111,"code":2175,"language":1113,"meta":22,"style":22},"using dddotNet.Domain.Accounts;\nusing dddotNet.Domain.Customers;\n\nnamespace dddotNet.Infrastructure.PersistenceLayer.SQLServer;\n\npublic class Account : Domain.Accounts.Account\n{\n    public Guid CustomerId { get; protected set; }\n\n    protected Account()\n    {\n    }\n\n    public Account(ICustomer customer)\n    {\n        Id = Guid.NewGuid();\n        CustomerId = customer.Id;\n    }\n\n    public void Load(IList\u003CCredit> credits, IList\u003CDebit> debits)\n    {\n        Credits = new CreditsCollection();\n        Credits.Add(credits);\n\n        Debits = new DebitsCollection();\n        Debits.Add(debits);\n    }\n}\n",[24,2177,2178,2182,2187,2191,2195,2199,2204,2208,2213,2217,2222,2226,2230,2234,2239,2243,2248,2253,2257,2261,2266,2270,2275,2280,2284,2289,2294,2298],{"__ignoreMap":22},[27,2179,2180],{"class":29,"line":30},[27,2181,1120],{},[27,2183,2184],{"class":29,"line":37},[27,2185,2186],{},"using dddotNet.Domain.Customers;\n",[27,2188,2189],{"class":29,"line":54},[27,2190,58],{"emptyLinePlaceholder":57},[27,2192,2193],{"class":29,"line":61},[27,2194,1356],{},[27,2196,2197],{"class":29,"line":67},[27,2198,58],{"emptyLinePlaceholder":57},[27,2200,2201],{"class":29,"line":159},[27,2202,2203],{},"public class Account : Domain.Accounts.Account\n",[27,2205,2206],{"class":29,"line":173},[27,2207,1143],{},[27,2209,2210],{"class":29,"line":178},[27,2211,2212],{},"    public Guid CustomerId { get; protected set; }\n",[27,2214,2215],{"class":29,"line":184},[27,2216,58],{"emptyLinePlaceholder":57},[27,2218,2219],{"class":29,"line":252},[27,2220,2221],{},"    protected Account()\n",[27,2223,2224],{"class":29,"line":263},[27,2225,1379],{},[27,2227,2228],{"class":29,"line":299},[27,2229,1394],{},[27,2231,2232],{"class":29,"line":355},[27,2233,58],{"emptyLinePlaceholder":57},[27,2235,2236],{"class":29,"line":361},[27,2237,2238],{},"    public Account(ICustomer customer)\n",[27,2240,2241],{"class":29,"line":404},[27,2242,1379],{},[27,2244,2245],{"class":29,"line":443},[27,2246,2247],{},"        Id = Guid.NewGuid();\n",[27,2249,2250],{"class":29,"line":449},[27,2251,2252],{},"        CustomerId = customer.Id;\n",[27,2254,2255],{"class":29,"line":489},[27,2256,1394],{},[27,2258,2259],{"class":29,"line":523},[27,2260,58],{"emptyLinePlaceholder":57},[27,2262,2263],{"class":29,"line":553},[27,2264,2265],{},"    public void Load(IList\u003CCredit> credits, IList\u003CDebit> debits)\n",[27,2267,2268],{"class":29,"line":558},[27,2269,1379],{},[27,2271,2272],{"class":29,"line":567},[27,2273,2274],{},"        Credits = new CreditsCollection();\n",[27,2276,2277],{"class":29,"line":601},[27,2278,2279],{},"        Credits.Add(credits);\n",[27,2281,2282],{"class":29,"line":625},[27,2283,58],{"emptyLinePlaceholder":57},[27,2285,2286],{"class":29,"line":655},[27,2287,2288],{},"        Debits = new DebitsCollection();\n",[27,2290,2291],{"class":29,"line":667},[27,2292,2293],{},"        Debits.Add(debits);\n",[27,2295,2296],{"class":29,"line":684},[27,2297,1394],{},[27,2299,2300],{"class":29,"line":693},[27,2301,1173],{},[10,2303,2304,2305,2307,2308,2311,2312,2315],{},"基础设施里定义了一个 ",[24,2306,1295],{}," 类，它",[1879,2309,2310],{},"继承并扩展","了领域层的 ",[24,2313,2314],{},"Domain.Accounts.Account","，领域层模型包含了核心的业务规则，比如Deposit存款、Withdraw取款，IsClosingAllowed校验、GetCurrentBalance计算余额。",[17,2317,2319],{"className":1111,"code":2318,"language":1113,"meta":22,"style":22},"using dddotNet.Domain.ValueObjects;\nusing Genocs.Common.CQRS.Events;\n\nnamespace dddotNet.Domain.Accounts;\n\npublic class Account : IAccount\n{\n    public Guid Id { get; protected set; }\n    public CreditsCollection Credits { get; protected set; }\n    public DebitsCollection Debits { get; protected set; }\n\n    public IReadOnlyCollection\u003CIEvent> DomainEvents => throw new NotImplementedException();\n\n    protected Account()\n    {\n        Credits = new CreditsCollection();\n        Debits = new DebitsCollection();\n    }\n\n    public ICredit Deposit(IEntityFactory entityFactory, PositiveMoney amountToDeposit)\n    {\n        var credit = entityFactory.NewCredit(this, amountToDeposit, DateTime.UtcNow);\n        Credits.Add(credit);\n        return credit;\n    }\n\n    public IDebit? Withdraw(IEntityFactory entityFactory, PositiveMoney amountToWithdraw)\n    {\n        if (GetCurrentBalance().LessThan(amountToWithdraw))\n            return null;\n\n        var debit = entityFactory.NewDebit(this, amountToWithdraw, DateTime.UtcNow);\n        Debits.Add(debit);\n        return debit;\n    }\n\n    public bool IsClosingAllowed()\n    {\n        return GetCurrentBalance().IsZero();\n    }\n\n    public Money GetCurrentBalance()\n    {\n        var totalCredits = Credits\n            .GetTotal();\n\n        var totalDebits = Debits\n            .GetTotal();\n\n        return totalCredits\n            .Subtract(totalDebits);\n    }\n\n    public bool IsTransient()\n    {\n        throw new NotImplementedException();\n    }\n\n    public void AddDomainEvent(IEvent @event)\n    {\n        throw new NotImplementedException();\n    }\n\n    public void ClearDomainEvents()\n    {\n        throw new NotImplementedException();\n    }\n}\n",[24,2320,2321,2326,2331,2335,2340,2344,2349,2353,2358,2363,2368,2372,2377,2381,2385,2389,2393,2397,2401,2405,2410,2414,2419,2424,2429,2433,2437,2442,2446,2451,2456,2460,2465,2470,2475,2479,2483,2488,2492,2497,2501,2505,2510,2514,2519,2524,2528,2533,2537,2541,2546,2551,2555,2559,2564,2568,2573,2577,2581,2586,2590,2594,2598,2602,2607,2611,2615,2619],{"__ignoreMap":22},[27,2322,2323],{"class":29,"line":30},[27,2324,2325],{},"using dddotNet.Domain.ValueObjects;\n",[27,2327,2328],{"class":29,"line":37},[27,2329,2330],{},"using Genocs.Common.CQRS.Events;\n",[27,2332,2333],{"class":29,"line":54},[27,2334,58],{"emptyLinePlaceholder":57},[27,2336,2337],{"class":29,"line":61},[27,2338,2339],{},"namespace dddotNet.Domain.Accounts;\n",[27,2341,2342],{"class":29,"line":67},[27,2343,58],{"emptyLinePlaceholder":57},[27,2345,2346],{"class":29,"line":159},[27,2347,2348],{},"public class Account : IAccount\n",[27,2350,2351],{"class":29,"line":173},[27,2352,1143],{},[27,2354,2355],{"class":29,"line":178},[27,2356,2357],{},"    public Guid Id { get; protected set; }\n",[27,2359,2360],{"class":29,"line":184},[27,2361,2362],{},"    public CreditsCollection Credits { get; protected set; }\n",[27,2364,2365],{"class":29,"line":252},[27,2366,2367],{},"    public DebitsCollection Debits { get; protected set; }\n",[27,2369,2370],{"class":29,"line":263},[27,2371,58],{"emptyLinePlaceholder":57},[27,2373,2374],{"class":29,"line":299},[27,2375,2376],{},"    public IReadOnlyCollection\u003CIEvent> DomainEvents => throw new NotImplementedException();\n",[27,2378,2379],{"class":29,"line":355},[27,2380,58],{"emptyLinePlaceholder":57},[27,2382,2383],{"class":29,"line":361},[27,2384,2221],{},[27,2386,2387],{"class":29,"line":404},[27,2388,1379],{},[27,2390,2391],{"class":29,"line":443},[27,2392,2274],{},[27,2394,2395],{"class":29,"line":449},[27,2396,2288],{},[27,2398,2399],{"class":29,"line":489},[27,2400,1394],{},[27,2402,2403],{"class":29,"line":523},[27,2404,58],{"emptyLinePlaceholder":57},[27,2406,2407],{"class":29,"line":553},[27,2408,2409],{},"    public ICredit Deposit(IEntityFactory entityFactory, PositiveMoney amountToDeposit)\n",[27,2411,2412],{"class":29,"line":558},[27,2413,1379],{},[27,2415,2416],{"class":29,"line":567},[27,2417,2418],{},"        var credit = entityFactory.NewCredit(this, amountToDeposit, DateTime.UtcNow);\n",[27,2420,2421],{"class":29,"line":601},[27,2422,2423],{},"        Credits.Add(credit);\n",[27,2425,2426],{"class":29,"line":625},[27,2427,2428],{},"        return credit;\n",[27,2430,2431],{"class":29,"line":655},[27,2432,1394],{},[27,2434,2435],{"class":29,"line":667},[27,2436,58],{"emptyLinePlaceholder":57},[27,2438,2439],{"class":29,"line":684},[27,2440,2441],{},"    public IDebit? Withdraw(IEntityFactory entityFactory, PositiveMoney amountToWithdraw)\n",[27,2443,2444],{"class":29,"line":693},[27,2445,1379],{},[27,2447,2448],{"class":29,"line":716},[27,2449,2450],{},"        if (GetCurrentBalance().LessThan(amountToWithdraw))\n",[27,2452,2453],{"class":29,"line":734},[27,2454,2455],{},"            return null;\n",[27,2457,2458],{"class":29,"line":753},[27,2459,58],{"emptyLinePlaceholder":57},[27,2461,2462],{"class":29,"line":772},[27,2463,2464],{},"        var debit = entityFactory.NewDebit(this, amountToWithdraw, DateTime.UtcNow);\n",[27,2466,2467],{"class":29,"line":788},[27,2468,2469],{},"        Debits.Add(debit);\n",[27,2471,2472],{"class":29,"line":796},[27,2473,2474],{},"        return debit;\n",[27,2476,2477],{"class":29,"line":827},[27,2478,1394],{},[27,2480,2481],{"class":29,"line":842},[27,2482,58],{"emptyLinePlaceholder":57},[27,2484,2485],{"class":29,"line":859},[27,2486,2487],{},"    public bool IsClosingAllowed()\n",[27,2489,2490],{"class":29,"line":882},[27,2491,1379],{},[27,2493,2494],{"class":29,"line":890},[27,2495,2496],{},"        return GetCurrentBalance().IsZero();\n",[27,2498,2499],{"class":29,"line":895},[27,2500,1394],{},[27,2502,2503],{"class":29,"line":1638},[27,2504,58],{"emptyLinePlaceholder":57},[27,2506,2507],{"class":29,"line":1644},[27,2508,2509],{},"    public Money GetCurrentBalance()\n",[27,2511,2512],{"class":29,"line":1650},[27,2513,1379],{},[27,2515,2516],{"class":29,"line":1656},[27,2517,2518],{},"        var totalCredits = Credits\n",[27,2520,2521],{"class":29,"line":1662},[27,2522,2523],{},"            .GetTotal();\n",[27,2525,2526],{"class":29,"line":1667},[27,2527,58],{"emptyLinePlaceholder":57},[27,2529,2530],{"class":29,"line":1673},[27,2531,2532],{},"        var totalDebits = Debits\n",[27,2534,2535],{"class":29,"line":1678},[27,2536,2523],{},[27,2538,2539],{"class":29,"line":1684},[27,2540,58],{"emptyLinePlaceholder":57},[27,2542,2543],{"class":29,"line":1690},[27,2544,2545],{},"        return totalCredits\n",[27,2547,2548],{"class":29,"line":1696},[27,2549,2550],{},"            .Subtract(totalDebits);\n",[27,2552,2553],{"class":29,"line":1701},[27,2554,1394],{},[27,2556,2557],{"class":29,"line":1706},[27,2558,58],{"emptyLinePlaceholder":57},[27,2560,2561],{"class":29,"line":1712},[27,2562,2563],{},"    public bool IsTransient()\n",[27,2565,2566],{"class":29,"line":1717},[27,2567,1379],{},[27,2569,2570],{"class":29,"line":1722},[27,2571,2572],{},"        throw new NotImplementedException();\n",[27,2574,2575],{"class":29,"line":1727},[27,2576,1394],{},[27,2578,2579],{"class":29,"line":1733},[27,2580,58],{"emptyLinePlaceholder":57},[27,2582,2583],{"class":29,"line":1739},[27,2584,2585],{},"    public void AddDomainEvent(IEvent @event)\n",[27,2587,2588],{"class":29,"line":1744},[27,2589,1379],{},[27,2591,2592],{"class":29,"line":1750},[27,2593,2572],{},[27,2595,2596],{"class":29,"line":1755},[27,2597,1394],{},[27,2599,2600],{"class":29,"line":1761},[27,2601,58],{"emptyLinePlaceholder":57},[27,2603,2604],{"class":29,"line":1766},[27,2605,2606],{},"    public void ClearDomainEvents()\n",[27,2608,2609],{"class":29,"line":1772},[27,2610,1379],{},[27,2612,2613],{"class":29,"line":1778},[27,2614,2572],{},[27,2616,2617],{"class":29,"line":1783},[27,2618,1394],{},[27,2620,2621],{"class":29,"line":1789},[27,2622,1173],{},[10,2624,2625,2626,2628,2629,2632,2633,2636],{},"这里领域模型（Domain Model）里的方法直接接收并调用了 ",[24,2627,1323],{},"，确实产生了一种",[1879,2630,2631],{},"依赖","。但从标准的企业级 DDD 架构来看，这种设计是",[1879,2634,2635],{},"刻意且合理","的。我们知道核心原则：",[78,2638,2639],{},[10,2640,2641],{},"领域层不能依赖基础设施层的具体实现（如 EF Core、数据库驱动、具体的 SQL 拼接等）。",[10,2643,2644,2645,2650,2651,2654,2655,2658,2659,2665,2666,2668,2669,2672],{},"回到刚才的基础设施，持久化层，这里定义了一个继承自领域模型的持久化子类，充当领域模型和关系型数据库的适配器。",[1879,2646,2647,2648],{},"领域模型中的 ",[24,2649,1295],{},"：为了保证业务逻辑的纯粹性，它可能只需要知道自己属于哪个客户（比如直接持有 ",[24,2652,2653],{},"ICustomer"," 引用，或者在创建时传入客户对象）。它不关心数据库里有没有 ",[24,2656,2657],{},"CustomerId"," 这个外键字段。",[1879,2660,2661,2662,2664],{},"数据库\u002FSQL Server 中的 ",[24,2663,1295],{}," 表","：关系型数据库是一个扁平的表格，它必须要有一个物理字段（",[24,2667,2657],{}," 列）来跟 ",[24,2670,2671],{},"Customer"," 表建立外键关联。",[10,2674,2675,2676,2679],{},"通过子类扩展出了 ",[24,2677,2678],{},"public Guid CustomerId { get; protected set; }","，让这个类能够完美映射到 SQL Server 的表结构上。",[1103,2681,2682],{"id":2682},"高频读和写操作分离",[10,2684,2685],{},"现代企业级高并发架构中，CQRS常常使用，读写分离。 在很多严肃的商业系统中，“用 EF Core 处理写，用 Dapper 处理读” 已经成为公认的最佳实践。",[10,2687,2688],{},"就用这个样板里的用户充值存款操作里来看：",[10,2690,2691],{},"入口是AccountsController.cs，控制器非常薄，只负责接收 HTTP 请求参数，构造 DepositInput 并传递给用例。",[17,2693,2695],{"className":1111,"code":2694,"language":1113,"meta":22,"style":22},"[ApiVersion(\"1.0\")]\n[Route(\"api\u002Fv1\u002F[controller]\")]\n[ApiController]\npublic sealed class AccountsController(IUseCase\u003CDepositInput> depositUseCase, DepositPresenter presenter) : ControllerBase\n{\n    private readonly IUseCase\u003CDepositInput> _depositUseCase = depositUseCase;\n    private readonly DepositPresenter _presenter = presenter;\n\n    \u002F\u002F\u002F \u003Csummary>\n    \u002F\u002F\u002F Deposit on an account.\n    \u002F\u002F\u002F \u003C\u002Fsummary>\n    \u002F\u002F\u002F \u003Cresponse code=\"200\">The updated balance.\u003C\u002Fresponse>\n    \u002F\u002F\u002F \u003Cresponse code=\"400\">Bad request.\u003C\u002Fresponse>\n    \u002F\u002F\u002F \u003Cresponse code=\"500\">Error.\u003C\u002Fresponse>\n    \u002F\u002F\u002F \u003Cparam name=\"request\">The request to deposit.\u003C\u002Fparam>\n    \u002F\u002F\u002F \u003Creturns>The updated balance.\u003C\u002Freturns>\n    [HttpPatch(\"Deposit\")]\n    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DepositResponse))]\n    [ProducesResponseType(StatusCodes.Status400BadRequest)]\n    [ProducesResponseType(StatusCodes.Status500InternalServerError)]\n    public async Task\u003CIActionResult?> DepositAsync([FromBody][Required] DepositRequest request)\n    {\n        var depositInput = new DepositInput(request.AccountId, new PositiveMoney(request.Amount));\n\n        await _depositUseCase.ExecuteAsync(depositInput);\n        return _presenter.ViewModel;\n    }\n}\n",[24,2696,2697,2702,2707,2712,2717,2721,2726,2731,2735,2740,2745,2750,2755,2760,2765,2770,2775,2780,2785,2790,2795,2800,2804,2809,2813,2818,2823,2827],{"__ignoreMap":22},[27,2698,2699],{"class":29,"line":30},[27,2700,2701],{},"[ApiVersion(\"1.0\")]\n",[27,2703,2704],{"class":29,"line":37},[27,2705,2706],{},"[Route(\"api\u002Fv1\u002F[controller]\")]\n",[27,2708,2709],{"class":29,"line":54},[27,2710,2711],{},"[ApiController]\n",[27,2713,2714],{"class":29,"line":61},[27,2715,2716],{},"public sealed class AccountsController(IUseCase\u003CDepositInput> depositUseCase, DepositPresenter presenter) : ControllerBase\n",[27,2718,2719],{"class":29,"line":67},[27,2720,1143],{},[27,2722,2723],{"class":29,"line":159},[27,2724,2725],{},"    private readonly IUseCase\u003CDepositInput> _depositUseCase = depositUseCase;\n",[27,2727,2728],{"class":29,"line":173},[27,2729,2730],{},"    private readonly DepositPresenter _presenter = presenter;\n",[27,2732,2733],{"class":29,"line":178},[27,2734,58],{"emptyLinePlaceholder":57},[27,2736,2737],{"class":29,"line":184},[27,2738,2739],{},"    \u002F\u002F\u002F \u003Csummary>\n",[27,2741,2742],{"class":29,"line":252},[27,2743,2744],{},"    \u002F\u002F\u002F Deposit on an account.\n",[27,2746,2747],{"class":29,"line":263},[27,2748,2749],{},"    \u002F\u002F\u002F \u003C\u002Fsummary>\n",[27,2751,2752],{"class":29,"line":299},[27,2753,2754],{},"    \u002F\u002F\u002F \u003Cresponse code=\"200\">The updated balance.\u003C\u002Fresponse>\n",[27,2756,2757],{"class":29,"line":355},[27,2758,2759],{},"    \u002F\u002F\u002F \u003Cresponse code=\"400\">Bad request.\u003C\u002Fresponse>\n",[27,2761,2762],{"class":29,"line":361},[27,2763,2764],{},"    \u002F\u002F\u002F \u003Cresponse code=\"500\">Error.\u003C\u002Fresponse>\n",[27,2766,2767],{"class":29,"line":404},[27,2768,2769],{},"    \u002F\u002F\u002F \u003Cparam name=\"request\">The request to deposit.\u003C\u002Fparam>\n",[27,2771,2772],{"class":29,"line":443},[27,2773,2774],{},"    \u002F\u002F\u002F \u003Creturns>The updated balance.\u003C\u002Freturns>\n",[27,2776,2777],{"class":29,"line":449},[27,2778,2779],{},"    [HttpPatch(\"Deposit\")]\n",[27,2781,2782],{"class":29,"line":489},[27,2783,2784],{},"    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DepositResponse))]\n",[27,2786,2787],{"class":29,"line":523},[27,2788,2789],{},"    [ProducesResponseType(StatusCodes.Status400BadRequest)]\n",[27,2791,2792],{"class":29,"line":553},[27,2793,2794],{},"    [ProducesResponseType(StatusCodes.Status500InternalServerError)]\n",[27,2796,2797],{"class":29,"line":558},[27,2798,2799],{},"    public async Task\u003CIActionResult?> DepositAsync([FromBody][Required] DepositRequest request)\n",[27,2801,2802],{"class":29,"line":567},[27,2803,1379],{},[27,2805,2806],{"class":29,"line":601},[27,2807,2808],{},"        var depositInput = new DepositInput(request.AccountId, new PositiveMoney(request.Amount));\n",[27,2810,2811],{"class":29,"line":625},[27,2812,58],{"emptyLinePlaceholder":57},[27,2814,2815],{"class":29,"line":655},[27,2816,2817],{},"        await _depositUseCase.ExecuteAsync(depositInput);\n",[27,2819,2820],{"class":29,"line":667},[27,2821,2822],{},"        return _presenter.ViewModel;\n",[27,2824,2825],{"class":29,"line":684},[27,2826,1394],{},[27,2828,2829],{"class":29,"line":693},[27,2830,1173],{},[10,2832,2833],{},"然后我们可以打开应用层的UseCase业务编排，看看Deposit.cs ：",[17,2835,2837],{"className":1111,"code":2836,"language":1113,"meta":22,"style":22},"public sealed class Deposit(\n                        IEntityFactory entityFactory,\n                        IOutputPort\u003CDepositOutput> outputHandler,\n                        IAccountRepository accountRepository,\n                        IUnitOfWork unitOfWork,\n                        IServiceBusClient serviceBus) : IUseCase\u003CDepositInput>\n{\n    private readonly IEntityFactory _entityFactory = entityFactory ?? throw new ArgumentNullException(nameof(entityFactory));\n    private readonly IOutputPort\u003CDepositOutput> _outputHandler = outputHandler ?? throw new ArgumentNullException(nameof(outputHandler));\n    private readonly IAccountRepository _accountRepository = accountRepository ?? throw new ArgumentNullException(nameof(accountRepository));\n    private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));\n    private readonly IServiceBusClient _serviceBus = serviceBus ?? throw new ArgumentNullException(nameof(serviceBus));\n\n    public async Task ExecuteAsync(DepositInput input, CancellationToken cancellationToken = default)\n    {\n        var account = await _accountRepository.GetAsync(input.AccountId, cancellationToken);\n        if (account == null)\n        {\n            _outputHandler.Error($\"The account {input.AccountId} does not exist or is already closed.\");\n            return;\n        }\n\n        var credit = account.Deposit(_entityFactory, input.Amount);\n\n        await _accountRepository.UpdateAsync(account, credit, cancellationToken);\n\n        await _serviceBus.PublishEventAsync(new Contracts.Events.DepositCompleted() { AccountId = input.AccountId, Amount = input.Amount.ToMoney().ToDecimal() }, cancellationToken);\n\n        await _unitOfWork.SaveAsync(cancellationToken);\n\n        DepositOutput output = new DepositOutput(\n            credit,\n            account.GetCurrentBalance());\n\n        _outputHandler.Default(output);\n    }\n}\n",[24,2838,2839,2844,2849,2854,2859,2864,2869,2873,2878,2883,2888,2893,2898,2902,2907,2911,2916,2921,2925,2930,2935,2939,2943,2948,2952,2957,2961,2966,2970,2975,2979,2984,2989,2994,2998,3003,3007],{"__ignoreMap":22},[27,2840,2841],{"class":29,"line":30},[27,2842,2843],{},"public sealed class Deposit(\n",[27,2845,2846],{"class":29,"line":37},[27,2847,2848],{},"                        IEntityFactory entityFactory,\n",[27,2850,2851],{"class":29,"line":54},[27,2852,2853],{},"                        IOutputPort\u003CDepositOutput> outputHandler,\n",[27,2855,2856],{"class":29,"line":61},[27,2857,2858],{},"                        IAccountRepository accountRepository,\n",[27,2860,2861],{"class":29,"line":67},[27,2862,2863],{},"                        IUnitOfWork unitOfWork,\n",[27,2865,2866],{"class":29,"line":159},[27,2867,2868],{},"                        IServiceBusClient serviceBus) : IUseCase\u003CDepositInput>\n",[27,2870,2871],{"class":29,"line":173},[27,2872,1143],{},[27,2874,2875],{"class":29,"line":178},[27,2876,2877],{},"    private readonly IEntityFactory _entityFactory = entityFactory ?? throw new ArgumentNullException(nameof(entityFactory));\n",[27,2879,2880],{"class":29,"line":184},[27,2881,2882],{},"    private readonly IOutputPort\u003CDepositOutput> _outputHandler = outputHandler ?? throw new ArgumentNullException(nameof(outputHandler));\n",[27,2884,2885],{"class":29,"line":252},[27,2886,2887],{},"    private readonly IAccountRepository _accountRepository = accountRepository ?? throw new ArgumentNullException(nameof(accountRepository));\n",[27,2889,2890],{"class":29,"line":263},[27,2891,2892],{},"    private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));\n",[27,2894,2895],{"class":29,"line":299},[27,2896,2897],{},"    private readonly IServiceBusClient _serviceBus = serviceBus ?? throw new ArgumentNullException(nameof(serviceBus));\n",[27,2899,2900],{"class":29,"line":355},[27,2901,58],{"emptyLinePlaceholder":57},[27,2903,2904],{"class":29,"line":361},[27,2905,2906],{},"    public async Task ExecuteAsync(DepositInput input, CancellationToken cancellationToken = default)\n",[27,2908,2909],{"class":29,"line":404},[27,2910,1379],{},[27,2912,2913],{"class":29,"line":443},[27,2914,2915],{},"        var account = await _accountRepository.GetAsync(input.AccountId, cancellationToken);\n",[27,2917,2918],{"class":29,"line":449},[27,2919,2920],{},"        if (account == null)\n",[27,2922,2923],{"class":29,"line":489},[27,2924,1611],{},[27,2926,2927],{"class":29,"line":523},[27,2928,2929],{},"            _outputHandler.Error($\"The account {input.AccountId} does not exist or is already closed.\");\n",[27,2931,2932],{"class":29,"line":553},[27,2933,2934],{},"            return;\n",[27,2936,2937],{"class":29,"line":558},[27,2938,1758],{},[27,2940,2941],{"class":29,"line":567},[27,2942,58],{"emptyLinePlaceholder":57},[27,2944,2945],{"class":29,"line":601},[27,2946,2947],{},"        var credit = account.Deposit(_entityFactory, input.Amount);\n",[27,2949,2950],{"class":29,"line":625},[27,2951,58],{"emptyLinePlaceholder":57},[27,2953,2954],{"class":29,"line":655},[27,2955,2956],{},"        await _accountRepository.UpdateAsync(account, credit, cancellationToken);\n",[27,2958,2959],{"class":29,"line":667},[27,2960,58],{"emptyLinePlaceholder":57},[27,2962,2963],{"class":29,"line":684},[27,2964,2965],{},"        await _serviceBus.PublishEventAsync(new Contracts.Events.DepositCompleted() { AccountId = input.AccountId, Amount = input.Amount.ToMoney().ToDecimal() }, cancellationToken);\n",[27,2967,2968],{"class":29,"line":693},[27,2969,58],{"emptyLinePlaceholder":57},[27,2971,2972],{"class":29,"line":716},[27,2973,2974],{},"        await _unitOfWork.SaveAsync(cancellationToken);\n",[27,2976,2977],{"class":29,"line":734},[27,2978,58],{"emptyLinePlaceholder":57},[27,2980,2981],{"class":29,"line":753},[27,2982,2983],{},"        DepositOutput output = new DepositOutput(\n",[27,2985,2986],{"class":29,"line":772},[27,2987,2988],{},"            credit,\n",[27,2990,2991],{"class":29,"line":788},[27,2992,2993],{},"            account.GetCurrentBalance());\n",[27,2995,2996],{"class":29,"line":796},[27,2997,58],{"emptyLinePlaceholder":57},[27,2999,3000],{"class":29,"line":827},[27,3001,3002],{},"        _outputHandler.Default(output);\n",[27,3004,3005],{"class":29,"line":842},[27,3006,1394],{},[27,3008,3009],{"class":29,"line":859},[27,3010,1173],{},[10,3012,3013],{},"这个代码比较精彩，可以仔细看，这个代码把输入\u002F输出端口模式使用上了，我们甚至可以直接脱离HTTP上下文，能够被API、GRPC甚至是控制台程序复用。\n它依赖仓储IAccountRepository 获取聚合根，调用聚合根的业务方法 account.Deposit(...)，最后调用 IUnitOfWork.SaveAsync() 统一提交事务。",[17,3015,3017],{"className":1111,"code":3016,"language":1113,"meta":22,"style":22},"await _accountRepository.UpdateAsync(...); \u002F\u002F 1. 更新\nawait _serviceBus.PublishEventAsync(...);  \u002F\u002F 2. 发消息到 RabbitMQ\u002FRedis\nawait _unitOfWork.SaveAsync(...);          \u002F\u002F 3. 提交数据库事务\n",[24,3018,3019,3024,3029],{"__ignoreMap":22},[27,3020,3021],{"class":29,"line":30},[27,3022,3023],{},"await _accountRepository.UpdateAsync(...); \u002F\u002F 1. 更新\n",[27,3025,3026],{"class":29,"line":37},[27,3027,3028],{},"await _serviceBus.PublishEventAsync(...);  \u002F\u002F 2. 发消息到 RabbitMQ\u002FRedis\n",[27,3030,3031],{"class":29,"line":54},[27,3032,3033],{},"await _unitOfWork.SaveAsync(...);          \u002F\u002F 3. 提交数据库事务\n",[10,3035,3036],{},"如果第二步消息发送成功了，但是第三步的数据库SaveAsyc因为网络抖动或者主键冲突导致失败了，或者是数据库回滚了，那么消费者就会收到存款成功但是没有落库的情况。",[10,3038,3039,3040],{},"那么怎么避免这种情况，那么应该使用",[1879,3041,3042],{},"事务性发件箱模式",[17,3044,3046],{"className":952,"code":3045,"language":954,"meta":22,"style":22},"flowchart TD\n    subgraph App[\"应用层 \u002F UseCase\"]\n        Business[\"1. 业务逻辑 (如存款)\"]\n        CreateOutbox[\"2. 创建待发事件消息\"]\n    end\n\n    subgraph Database[\"同一个 SQL Server 数据库事务\"]\n        AccountTable[(\"Account \u002F Credit 表\")]\n        OutboxTable[(\"OutboxMessages 表 (发件箱)\")]\n    end\n\n    subgraph BackgroundWorker[\"后台发布进程 (Outbox Processor)\"]\n        Poller[\"定期拉取未发送消息 \u002F CDC\"]\n        MarkSent[\"标记消息为已发送\"]\n    end\n\n    subgraph Broker[\"消息代理 (RabbitMQ)\"]\n        RabbitMQ[\"RabbitMQ 交换机\u002F队列\"]\n    end\n\n    Business -->|更新业务数据| AccountTable\n    CreateOutbox -->|插入待发消息| OutboxTable\n\n    %% 这里加上了双引号，修复了解析错误\n    AccountTable -.->|\"【同一个 SaveAsync() 本地事务】\u003Cbr\u002F>要么同时成功，要么同时回滚！\"| OutboxTable\n\n    Poller -->|读取未发送消息| OutboxTable\n    Poller -->|投递事件| RabbitMQ\n    RabbitMQ -->|ACK 确认| MarkSent\n    MarkSent -->|更新状态为 Processed| OutboxTable\n",[24,3047,3048,3052,3057,3062,3067,3072,3076,3081,3086,3091,3095,3099,3104,3109,3114,3118,3122,3127,3132,3136,3140,3145,3150,3154,3159,3164,3168,3173,3178,3183],{"__ignoreMap":22},[27,3049,3050],{"class":29,"line":30},[27,3051,961],{},[27,3053,3054],{"class":29,"line":37},[27,3055,3056],{},"    subgraph App[\"应用层 \u002F UseCase\"]\n",[27,3058,3059],{"class":29,"line":54},[27,3060,3061],{},"        Business[\"1. 业务逻辑 (如存款)\"]\n",[27,3063,3064],{"class":29,"line":61},[27,3065,3066],{},"        CreateOutbox[\"2. 创建待发事件消息\"]\n",[27,3068,3069],{"class":29,"line":67},[27,3070,3071],{},"    end\n",[27,3073,3074],{"class":29,"line":159},[27,3075,58],{"emptyLinePlaceholder":57},[27,3077,3078],{"class":29,"line":173},[27,3079,3080],{},"    subgraph Database[\"同一个 SQL Server 数据库事务\"]\n",[27,3082,3083],{"class":29,"line":178},[27,3084,3085],{},"        AccountTable[(\"Account \u002F Credit 表\")]\n",[27,3087,3088],{"class":29,"line":184},[27,3089,3090],{},"        OutboxTable[(\"OutboxMessages 表 (发件箱)\")]\n",[27,3092,3093],{"class":29,"line":252},[27,3094,3071],{},[27,3096,3097],{"class":29,"line":263},[27,3098,58],{"emptyLinePlaceholder":57},[27,3100,3101],{"class":29,"line":299},[27,3102,3103],{},"    subgraph BackgroundWorker[\"后台发布进程 (Outbox Processor)\"]\n",[27,3105,3106],{"class":29,"line":355},[27,3107,3108],{},"        Poller[\"定期拉取未发送消息 \u002F CDC\"]\n",[27,3110,3111],{"class":29,"line":361},[27,3112,3113],{},"        MarkSent[\"标记消息为已发送\"]\n",[27,3115,3116],{"class":29,"line":404},[27,3117,3071],{},[27,3119,3120],{"class":29,"line":443},[27,3121,58],{"emptyLinePlaceholder":57},[27,3123,3124],{"class":29,"line":449},[27,3125,3126],{},"    subgraph Broker[\"消息代理 (RabbitMQ)\"]\n",[27,3128,3129],{"class":29,"line":489},[27,3130,3131],{},"        RabbitMQ[\"RabbitMQ 交换机\u002F队列\"]\n",[27,3133,3134],{"class":29,"line":523},[27,3135,3071],{},[27,3137,3138],{"class":29,"line":553},[27,3139,58],{"emptyLinePlaceholder":57},[27,3141,3142],{"class":29,"line":558},[27,3143,3144],{},"    Business -->|更新业务数据| AccountTable\n",[27,3146,3147],{"class":29,"line":567},[27,3148,3149],{},"    CreateOutbox -->|插入待发消息| OutboxTable\n",[27,3151,3152],{"class":29,"line":601},[27,3153,58],{"emptyLinePlaceholder":57},[27,3155,3156],{"class":29,"line":625},[27,3157,3158],{},"    %% 这里加上了双引号，修复了解析错误\n",[27,3160,3161],{"class":29,"line":655},[27,3162,3163],{},"    AccountTable -.->|\"【同一个 SaveAsync() 本地事务】\u003Cbr\u002F>要么同时成功，要么同时回滚！\"| OutboxTable\n",[27,3165,3166],{"class":29,"line":667},[27,3167,58],{"emptyLinePlaceholder":57},[27,3169,3170],{"class":29,"line":684},[27,3171,3172],{},"    Poller -->|读取未发送消息| OutboxTable\n",[27,3174,3175],{"class":29,"line":693},[27,3176,3177],{},"    Poller -->|投递事件| RabbitMQ\n",[27,3179,3180],{"class":29,"line":716},[27,3181,3182],{},"    RabbitMQ -->|ACK 确认| MarkSent\n",[27,3184,3185],{"class":29,"line":734},[27,3186,3187],{},"    MarkSent -->|更新状态为 Processed| OutboxTable\n",[10,3189,3190],{},"这个在其他笔记里看，请查看[[MassTransitService收件箱模式]]。",[921,3192,3193],{"id":3193},"消息队列与弹性通信",[10,3195,3196],{},"• Rebus.RabbitMq (10.1.1)：轻量级服务总线框架 Rebus 的 RabbitMQ 传输实现，用于事件驱动和异步解耦。\n• Azure.Messaging.ServiceBus (7.20.1) \u002F Microsoft.Azure.ServiceBus (5.2.0)：Azure 服务总线集成（在 Worker 中）。\n• Microsoft.Extensions.Http.Polly (10.0.7)：提供 HTTP 请求重试、熔断与超时等弹性策略。\n• Refit.HttpClientFactory (10.1.6)：类型安全的声明式 REST HTTP 客户端。",[921,3198,3200],{"id":3199},"api-与运行宿主api-management-host","API 与运行宿主（API Management & Host）",[10,3202,3203],{},"在 Host.csproj 中：",[10,3205,3206],{},"• Asp.Versioning.Mvc.ApiExplorer (8.1.1)：RESTful API 多版本控制与 Swagger 探索支持。\n• Microsoft.FeatureManagement.AspNetCore (4.5.0)：功能开关（Feature Flags）管理。\n• EPPlus (8.5.4)：Excel 报表生成与导出处理。",[921,3208,3210],{"id":3209},"健康检查与监控healthchecks-diagnostics","健康检查与监控（HealthChecks & Diagnostics）",[10,3212,3213],{},"• AspNetCore.HealthChecks.RabbitMQ (9.0.0) & AspNetCore.HealthChecks.Uris (9.0.0)：健康检查端点探针。\n• Microsoft.ApplicationInsights.Kubernetes (8.0.1)：K8s 环境下的 Application Insights 遥测强化。",[921,3215,3217],{"id":3216},"全局代码质量与测试build-tests","全局代码质量与测试（Build & Tests）",[10,3219,3220],{},"• 通过 Directory.Build.props 全局注入：\n• Roslynator.Analyzers (4.15.0)：C# 静态代码分析与优化建议。\n• StyleCop.Analyzers (1.2.0-beta.556)：代码规范与风格审查。\n• 单元测试与集成测试：\n• xunit (2.9.3) & Microsoft.NET.Test.Sdk (18.5.1)",[3222,3223,3224],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":22,"searchDepth":37,"depth":37,"links":3226},[3227,3228,3233,3234,3235,3236],{"id":923,"depth":37,"text":923},{"id":932,"depth":37,"text":932,"children":3229},[3230,3231,3232],{"id":1105,"depth":54,"text":1105},{"id":1346,"depth":54,"text":1346},{"id":2682,"depth":54,"text":2682},{"id":3193,"depth":37,"text":3193},{"id":3199,"depth":37,"text":3200},{"id":3209,"depth":37,"text":3210},{"id":3216,"depth":37,"text":3217},"md",{"date":3239,"tags":3240},"2026-08-05",[3241,3242,3243,3244],"architecture","csharp","genocs","ddd","\u002Farchitecture\u002F初识cleanarchitecture架构项目",{"description":12},"architecture\u002F初识CleanArchitecture架构项目","Mz_gB0cLahrdOaVeZdVrUkDtwz8_xkV9RTCUWeamCa4",1792926849910]