)
dotnet-starter-kit 模块化架构实战四步注册法新增业务模块Add Module 完全指南【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit导读本指南基于 dotnet-starter-kit 官方开发技能文档.agents/skills/add-module/SKILL.md展开讲解在 FSH.Starter 模块化单体Modular Monolith Vertical Slice Architecture中新增一个独立业务模块Bounded Context的完整流程。读完本文你将掌握模块双工程runtime Contracts的项目结构、[FshModule]程序集级声明、IModule三阶段生命周期、权限/DbContext/迁移的接入方式以及本仓库最具陷阱性的「四处注册」规则——遗漏任意一处都会导致模块静默失效。本文所有结论均可在仓库源码与测试中找到对应依据。一、模块是什么runtime 与 Contracts 的双工程结构在 dotnet-starter-kit 中每个业务模块都由两个 .NET 工程组成分别承载内部实现与对外公共契约src/Modules/{Name}/ ├── Modules.{Name}/ ← runtime内部Domain/、Data/、Features/v1/、{Name}Module.cs └── Modules.{Name}.Contracts/ ← public APIv1/commands/queries、Dtos/、Authorization/、Events/runtime 工程Modules.{Name}存放领域实体、EF Core DbContext、CQRS 处理器、Minimal API 端点等内部实现属于模块的私有实现细节Contracts 工程Modules.{Name}.Contracts存放公开的命令/查询类型、DTO、权限定义与集成事件是模块对外的公共 API 面。这一拆分与.agents/rules/architecture.md中定义的依赖方向严格一致Host组合根 → Modules.{Name}runtime → Modules.{Name}.Contractspublic API → BuildingBlocks共享框架架构规则同时强调一个模块的 runtime 工程不得引用另一个模块的 runtime 工程只能引用其 Contracts该约束由Architecture.Tests基于 NetArchTest强制校验。跨模块通信只能走 Contracts 中的服务接口或集成事件。复制而非手写 csprojSKILL 文档给出了一条非常实用的建议直接复制现有模块如Modules.Catalog的两个.csproj文件再重命名不要手写工程引用。原因很直接runtime 工程需要正确引用自己的 Contracts 工程以及所需的 BuildingBlocks如 Persistence、Web 等Contracts 工程需要引用Mediator与共享契约这些引用关系极易拼错或遗漏。复制现有成熟模块的 csproj 可以保证引用拓扑从一开始就是正确的。仓库中的真实范例可参考src/Modules/Catalog/其 runtime 工程 CatalogModule.cs 与 Contracts 工程含 CatalogPermissions.cs正是 SKILL 中所有示例的原型来源。二、Step 1[FshModule]是程序集级特性不是类级特性模块的注册入口是一个程序集级assembly-level特性必须写在命名空间声明上方[assembly: FshModule(typeof(FSH.Modules.{Name}.{Name}Module), 900)] // (Type, order) namespace FSH.Modules.{Name}; public sealed class {Name}Module : IModule { public void ConfigureServices(IHostApplicationBuilder builder) { ArgumentNullException.ThrowIfNull(builder); PermissionConstants.Register({Name}Permissions.All); builder.Services.AddHeroDbContext{Name}DbContext(); builder.Services.AddScopedIDbInitializer, {Name}DbInitializer(); // Only if the module HANDLES integration events: // builder.Services.AddIntegrationEventHandlers(typeof({Name}Module).Assembly); // // Publishing needs no registration at all — the outbox is framework-owned // (host calls AddEventingCore once). Inject IOutboxWriter and publish. // Never register a per-module outbox store; see .agents/rules/eventing.md. builder.Services.AddHealthChecks() .AddDbContextCheck{Name}DbContext(name: db:{name}); } public void ConfigureMiddleware(IApplicationBuilder app) { } // optional, runs after auth public void MapEndpoints(IEndpointRouteBuilder endpoints) { ArgumentNullException.ThrowIfNull(endpoints); var versionSet endpoints.NewApiVersionSet().HasApiVersion(new ApiVersion(1)).ReportApiVersions().Build(); var group endpoints.MapGroup(api/v{version:apiVersion}/{name}) .WithTags({Name}).WithApiVersionSet(versionSet).RequireAuthorization(); // group.MapCreate{Entity}Endpoint(); … } }为什么必须用位置参数[FshModule]特性的定义位于 FshModuleAttribute.cs关键点有二AttributeUsage(AttributeTargets.Assembly, AllowMultiple true)——它只允许作用于程序集且可重复标注构造函数签名为FshModuleAttribute(Type moduleType, int order 0)——两个参数都是位置参数SKILL 特别强调应写作[assembly: FshModule(typeof(...), 900)]而非[assembly: FshModule(Order n)]命名参数写法在程序集级特性的既有代码风格中不被采用。Order决定模块加载顺序Order控制模块的启动排序数值越小越先执行。仓库中现有模块的取值如下模块OrderAuditing300Files350Webhooks400Billing500Catalog600Tickets700Notifications750Chat800例如 CatalogModule.cs 顶部声明为[assembly: FshModule(typeof(FSH.Modules.Catalog.CatalogModule), 600)]。如果新模块要消费其他模块发布的事件它的 Order 必须大于被依赖模块确保对方先完成注册。加载器如何发现并实例化模块ModuleLoaderModuleLoader.cs是这一切的幕后执行者从传入的程序集集合中扫描所有FshModuleAttribute过滤出IModule可赋值的目标类型按Order升序、再按模块类型名排序去重逐个Activator.CreateInstance实例化并调用ConfigureServices。ModuleLoader使用lock保证线程安全ConfigureServices只执行一次。随后UseModuleMiddlewares会在认证之后调用各模块的ConfigureMiddlewareMapModules则负责把所有模块的MapEndpoints挂载到路由表。IModule 三阶段生命周期每个模块类实现IModule接口共三个方法ConfigureServices(IHostApplicationBuilder builder)注册 DI 服务、权限、DbContext、健康检查与事件处理器是模块的「组合根」入口ConfigureMiddleware(IApplicationBuilder app)可选注册模块级中间件在UseAuthentication之后执行MapEndpoints(IEndpointRouteBuilder endpoints)映射模块的 Minimal API 端点统一挂在api/v{version:apiVersion}/{name}前缀下并附加版本集与RequireAuthorization()。SKILL 中MapEndpoints的示例与真实 CatalogModule.cs 的写法完全一致先构造 API 版本集NewApiVersionSet().HasApiVersion(new ApiVersion(1)).ReportApiVersions().Build()再MapGroup(api/v{version:apiVersion}/catalog)最后逐一调用各端点的MapXxxEndpoint()扩展方法。Catalog 模块中还有一个值得学习的细节trash 与 tree 这类字面量路由必须先于/{id:guid}通配路由注册否则字面量段会被通配路由抢先匹配。三、Step 2权限定义Contracts/Authorization模块的权限通过{Name}Permissions静态类声明放在Contracts 工程的Authorization/目录下由嵌套的资源类 一个All集合组成最终通过PermissionConstants.Register({Name}Permissions.All)在ConfigureServices中注册。参照 CatalogPermissions.cs 的完整形态public static class CatalogPermissions { public static class Products { public const string Resource Catalog.Products; public const string View $Permissions.{Resource}.View; public const string Create $Permissions.{Resource}.Create; public const string Update $Permissions.{Resource}.Update; public const string Delete $Permissions.{Resource}.Delete; public const string Restore $Permissions.{Resource}.Restore; public const string AdjustStock $Permissions.{Resource}.AdjustStock; } public static IReadOnlyListFshPermission All { get; } [ new(View Products, ActionConstants.View, Products.Resource, IsBasic: true), new(Create Products, ActionConstants.Create, Products.Resource), // … ]; }要点每个资源Brands / Categories / Products…一个嵌套类权限字符串统一为Permissions.{Resource}.{Action}格式All是IReadOnlyListFshPermission每项由显示名、Action、Resource三元组构成IsBasic: true标记基础权限将这些权限放进 Contracts 工程是因为前端admin/dashboard 两个 React 客户端与权限校验代码都需要引用这一份公共定义避免运行时工程与前端各自维护一份。四、Step 3DbContext 继承BaseDbContext模块的 EF Core DbContext 必须继承框架的BaseDbContext采用固定四参构造函数并遵循严格的OnModelCreating覆盖顺序public sealed class {Name}DbContext : BaseDbContext { public const string Schema {name}; public {Name}DbContext( IMultiTenantContextAccessorAppTenantInfo multiTenantContextAccessor, DbContextOptions{Name}DbContext options, IOptionsDatabaseOptions settings, IHostEnvironment environment) : base(multiTenantContextAccessor, options, settings, environment) { } public DbSet{Entity} {Entities} Set{Entity}(); protected override void OnModelCreating(ModelBuilder modelBuilder) { ArgumentNullException.ThrowIfNull(modelBuilder); modelBuilder.HasDefaultSchema(Schema); modelBuilder.ApplyConfigurationsFromAssembly(typeof({Name}DbContext).Assembly); base.OnModelCreating(modelBuilder); // MUST be last — applies tenant soft-delete filters } }这里有三条不可违背的约定四参构造函数IMultiTenantContextAccessorAppTenantInfo、DbContextOptionsT、IOptionsDatabaseOptions、IHostEnvironment缺一不可。创建迁移时dotnet ef之所以能对BaseDbContext派生类型生效正是因为这四个参数可以由启动宿主API host的 DI 容器满足见 create-migration SKILL 的 Notes 部分。base.OnModelCreating(modelBuilder)必须放在最后基类在这里追加租户隔离过滤与软删除过滤对应Persistence中的QueryFilters一旦调整顺序租户隔离与软删除规则就会被覆盖而失效。显式 schemaHasDefaultSchema(Schema)让每个模块的表落在独立的 PostgreSQL schema如catalog中这是多租户 多模块共存时的隔离基础。对应地ConfigureServices中要完成两项注册builder.Services.AddHeroDbContext{Name}DbContext(); builder.Services.AddScopedIDbInitializer, {Name}DbInitializer();AddHeroDbContextT()注册 DbContext{Name}DbInitializer则负责在启动/迁移时为该模块执行种子等初始化逻辑。五、Step 4解决方案与工程引用把两个新工程加入解决方案文件并建立宿主对它们的引用dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj随后需要为以下工程添加ProjectReferenceruntime 模块工程同时被FSH.Starter.Api和FSH.Starter.DbMigrator引用FSH.Starter.Migrations.PostgreSQL引用 runtime 模块工程迁移需要加载模块程序集来发现实体与配置。六、Step 5集中式迁移工程中的模块子目录dotnet-starter-kit 的迁移采用单一工程、按模块分目录的组织方式所有 EF Core 迁移都放在src/Host/FSH.Starter.Migrations.PostgreSQL但按{Name}/目录Catalog/、Identity/、Billing/等分文件夹存放每个 DbContext 拥有独立的{X}DbContextModelSnapshot。新增模块时在迁移工程下新建{Name}/文件夹然后用 create-migration SKILL 的标准流程生成初始迁移——注意必须同时指定--project、--startup-project与--context {Name}DbContext三个参数并用--output-dir {Name}让迁移落入模块专属文件夹dotnet build src/FSH.Starter.slnx # 先构建快照基于构建结果重新生成 dotnet ef migrations add {MigrationName} \ --project src/Host/FSH.Starter.Migrations.PostgreSQL \ --startup-project src/Host/FSH.Starter.Api \ --context {Name}DbContext \ --output-dir {Name}创建后建议先用dotnet ef migrations script --idempotent审查生成的 SQL确认没有意外的删表、向已有表添加无默认值的非空列、或以 dropadd 形式呈现的改名这些都会造成数据丢失再通过 DbMigrator 应用dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending # 先预览 dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply # 迁移租户目录 每个租户的模块 schema数据库不会在 API 启动时自动迁移迁移由DbMigrator宿主统一执行。七、Step 6⚠️ 四处注册——整个流程最大的坑SKILL 文档把这一步标记为 high-ceremony 的核心所在「一个模块必须被接线到四个位置」。遗漏任何一处都会导致静默失败不报错、不警告只是功能不生效。需要在src/Host/FSH.Starter.Api/Program.cs和src/Host/FSH.Starter.DbMigrator/Program.cs两处做完全相同的两处编辑1. Mediator 的o.Assemblies需要两个标记一个 Contracts 类型如typeof(FSH.Modules.{Name}.Contracts.{Name}ContractsMarker)加上模块类型typeof({Name}Module)。以 FSH.Starter.Api/Program.cs 中的真实代码为例每个模块都贡献一对条目builder.Services.AddMediator(o { o.ServiceLifetime ServiceLifetime.Scoped; o.Assemblies [ // …框架与其他模块的标记… typeof(FSH.Modules.Billing.Contracts.BillingContractsMarker), typeof(FSH.Modules.Billing.BillingModule), typeof(FSH.Modules.Catalog.Contracts.CatalogContractsMarker), typeof(FSH.Modules.Catalog.CatalogModule), // …… ]; });说明Contracts 标记并不强制命名为{Name}ContractsMarker如 Webhooks 模块使用的是CreateWebhookSubscriptionCommand、Files 使用RequestUploadUrlCommand、Chat 使用CreateChannelCommandSKILL 建议的{Name}ContractsMarker是一种约定俗成的简洁做法只要确保「一个 Contracts 程序集中的类型 模块 runtime 类型」成对出现即可。Mediator 程序集列表负责让 CQRS 处理器被正确发现。2.moduleAssemblies数组追加typeof({Name}Module).Assemblyvar moduleAssemblies new Assembly[] { typeof(IdentityModule).Assembly, typeof(MultitenancyModule).Assembly, // …… typeof({Name}Module).Assembly, };随后通过builder.AddModules(moduleAssemblies)即上文ModuleLoader.AddModules把模块装配进 DI 容器。遗漏各位置的后果速查表遗漏位置后果Mediatoro.Assemblies中的 Contracts 标记命令/查询处理器被静默遗漏接口返回但业务不执行Mediatoro.Assemblies中的模块类型标记同上处理器发现不完整moduleAssemblies数组条目Api模块从未被加载端点 404DbMigrator 中对应的 Mediator moduleAssemblies 两处migrate/seed 跳过该模块schema 与种子数据缺失本仓库的架构规则文档.agents/rules/architecture.md也以表格形式记录了同样的四处接线要求并给出了最快的自检方式构建通过后直接访问新端点确认处理器真的执行。关于事件总线注册的澄清SKILL 同时澄清了一个常见误区仅当模块需要消费处理集成事件时才需要builder.Services.AddIntegrationEventHandlers(typeof({Name}Module).Assembly)发布事件不需要任何模块级注册——事务性 Outbox 是框架基础设施由宿主调用一次AddEventingCore统一注册API 与 DbMigrator 的Program.cs中均有此调用见 FSH.Starter.Api/Program.cs 与 FSH.Starter.DbMigrator/Program.cs。模块只需注入IOutboxWriter即可发布永远不要注册 per-module 的 outbox store。八、Step 7验证与检查清单模块接线完成后用三条命令做最终验证dotnet build src/FSH.Starter.slnx # 0 warnings dotnet test src/Tests/Architecture.Tests # boundary tenant-isolation rules must pass dotnet test src/FSH.Starter.slnx其中Architecture.Tests尤为重要——它用 NetArchTest 强制校验边界规则模块不得引用其他模块 runtime 工程与租户隔离规则是模块化架构不被破坏的自动化防线。仓库在src/Tests/Architecture.Tests/下还提供了 ModuleArchitectureTests.cs、TenantIsolationTests.cs、LayerDependencyTests.cs 等专项测试。完整检查清单两个工程复制自现有模块的 csproj已加入.slnx被 Api DbMigrator Migrations引用[assembly: FshModule(typeof({Name}Module), order)]程序集级、位置参数IModuleAddHeroDbContextT()、PermissionConstants.Register、版本集分组、需要时的事件处理器三件套{Name}DbContext : BaseDbContext四参构造函数base.OnModelCreating放在最后{Name}Permissions位于 Contracts/AuthorizationMigrations 文件夹 初始迁移--context {Name}DbContext已在全部四个位置注册Api DbMigrator × Mediator moduleAssembliesBuild Architecture.Tests 全绿九、相关技能与深入阅读本指南是仓库 Agent 技能体系的一部分与以下技能/规则文档联动使用create-migration SKILL生成与应用迁移的完整流程构建先行、--context与--output-dir、SQL 审查、DbMigrator applyarchitecture rules模块依赖方向、IModule 注册约定、中间件顺序、四处注册陷阱的权威定义eventing rules框架自有事务性 Outbox 的消费/发布注册边界database rules多租户数据库与迁移组织方式add-entity SKILL 与 add-feature SKILL在既有模块内新增实体与功能切片的配套流程真实模块范例CatalogModule.cs、CatalogPermissions.cs加载器与特性实现ModuleLoader.cs、FshModuleAttribute.cs宿主接线实况FSH.Starter.Api/Program.cs、FSH.Starter.DbMigrator/Program.cs。总结在 dotnet-starter-kit 中新增一个业务模块本质上是在「双工程结构 程序集级特性 集中式迁移 四处接线」这四条主线上完成一次高确定性的装配。最容易出错的是 Step 6 的四处注册Mediator 双标记与moduleAssemblies必须在 Api 与 DbMigrator 两个宿主中各出现一次缺一即静默失效。按本文的七个步骤与检查清单执行并用Architecture.Tests守住边界与租户隔离规则即可安全、可验证地完成模块化扩展。【免费下载链接】dotnet-starter-kitProduction Grade Cloud-Ready .NET 10 Starter Kit (Web API React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200 Development Hours! All Batteries Included.项目地址: https://gitcode.com/GitHub_Trending/do/dotnet-starter-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考