教程:从零创建一个业务模块
本教程创建一个最小的 Notice 模块,并完整接入当前主宿主。示例遵循仓库正在使用的边界:
FreeKit.Notice:实体、Manager、Application Service、契约和 Core Startup。FreeKit.Notice.HttpApi:Controller 和 HttpApi Startup。FreeKit.DI:后台 Host 可复用的 Core 模块清单。FreeKit.Host:同时装配 Core 和 HttpApi。
最终接口为 POST /api/notice/notices,归入现有 Swagger v1 文档。
1. 创建两个项目
目录:
src/Services/Notice/
├── FreeKit.Notice/
│ ├── Application/Notices/
│ ├── Contracts/
│ ├── Domain/Notices/
│ ├── FreeKit.Notice.csproj
│ └── NoticeModuleStartup.cs
└── FreeKit.Notice.HttpApi/
├── Controllers/
├── FreeKit.Notice.HttpApi.csproj
└── NoticeHttpApiModuleStartup.cs
Core 项目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\..\..\BuildingBlocks\IGeekFan.FreeKit.Web\IGeekFan.FreeKit.Web.csproj" />
</ItemGroup>
</Project>
HttpApi 项目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\FreeKit.Notice\FreeKit.Notice.csproj" />
</ItemGroup>
</Project>
将两个项目加入根解决方案:
dotnet sln FreeKitModules.slnx add src/Services/Notice/FreeKit.Notice/FreeKit.Notice.csproj
dotnet sln FreeKitModules.slnx add src/Services/Notice/FreeKit.Notice.HttpApi/FreeKit.Notice.HttpApi.csproj
2. 定义实体
using IGeekFan.FreeKit.Extras.AuditEntity;
namespace FreeKit.Notice.Domain.Notices;
public sealed class NoticeItem : FullAuditEntity
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime PublishedAt { get; set; }
}
FullAuditEntity 使用 Guid 主键并包含创建、修改、删除审计和软删除字段。PublishedAt 是业务时间,后面显式写入 DateTime.UtcNow。
3. 定义契约和权限
using System.ComponentModel.DataAnnotations;
using IGeekFan.FreeKit.Infrastructure.Application;
namespace FreeKit.Notice.Contracts;
public sealed class PublishNoticeRequest
{
[Required]
[MaxLength(200)]
public string Title { get; set; } = string.Empty;
[Required]
public string Content { get; set; } = string.Empty;
}
public sealed class NoticeDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime PublishedAt { get; set; }
}
public interface INoticeService : IApplicationService
{
Task<NoticeDto> PublishAsync(PublishNoticeRequest request);
}
public static class NoticePermissions
{
public const string GroupName = "Notice";
public static class Notices
{
public const string Default = GroupName + ".Notices";
public const string Create = Default + ".Create";
}
}
公开 DTO 不使用 [JsonPropertyName],因为主 API 边界由 Newtonsoft.Json 处理。
4. 实现 Domain Manager
using IGeekFan.FreeKit.Extras.FreeSql;
using IGeekFan.FreeKit.Infrastructure.DDD;
using IGeekFan.FreeKit.Infrastructure.Exceptions;
namespace FreeKit.Notice.Domain.Notices;
public interface INoticeManager : IDomainService
{
Task<NoticeItem> PublishAsync(
string title,
string content);
}
public sealed class NoticeManager(
IAuditBaseRepository<NoticeItem> repository)
: DomainService, INoticeManager
{
public async Task<NoticeItem> PublishAsync(
string title,
string content)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new BusinessException("公告标题不能为空");
}
var notice = new NoticeItem
{
Id = Guid.NewGuid(),
Title = title.Trim(),
Content = content,
PublishedAt = DateTime.UtcNow
};
return await repository.InsertAsync(notice);
}
}
Manager 通过 IAuditBaseRepository<NoticeItem> 持久化,不向 Application 或 Controller 暴露 IFreeSql。
5. 实现 Application Service
using FreeKit.Notice.Contracts;
using FreeKit.Notice.Domain.Notices;
using IGeekFan.FreeKit.Infrastructure.Application;
namespace FreeKit.Notice.Application.Notices;
public sealed class NoticeService(INoticeManager manager)
: ApplicationService, INoticeService
{
public async Task<NoticeDto> PublishAsync(
PublishNoticeRequest request)
{
NoticeItem notice = await manager.PublishAsync(
request.Title,
request.Content);
return new NoticeDto
{
Id = notice.Id,
Title = notice.Title,
Content = notice.Content,
PublishedAt = notice.PublishedAt
};
}
}
Application Service 负责用例编排和 DTO 转换,Controller 不直接调用 Manager。
6. 创建 Core Startup
using FreeKit.Notice.Application.Notices;
using FreeKit.Notice.Contracts;
using FreeKit.Notice.Domain.Notices;
using FreeSql;
using IGeekFan.FreeKit.Modularity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace FreeKit.Notice;
public sealed class NoticeModuleStartup : IModuleStartup
{
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration)
{
services.AddScoped<INoticeManager, NoticeManager>();
services.AddScoped<INoticeService, NoticeService>();
}
public void Configure(
WebApplication app,
IWebHostEnvironment env)
{
#if DEBUG
IFreeSql freeSql = app.Services.GetRequiredService<IFreeSql>();
freeSql.CodeFirst.SyncStructure<NoticeItem>();
#endif
}
}
两个接口方法都必须实现。示例在 DEBUG 下同步表结构,Release 部署必须使用正式迁移或预建表,不能依赖这段代码。
开放泛型 IAuditBaseRepository<> 已由 Web BuildingBlock 注册,不存在也不需要调用 AddFreeRepository(typeof(NoticeItem).Assembly)。
7. 创建 Controller
using FreeKit.Notice.Contracts;
using IGeekFan.FreeKit.Infrastructure.Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace FreeKit.Notice.HttpApi.Controllers;
[ApiExplorerSettings(GroupName = "v1")]
[Route("api/notice/notices")]
[ApiController]
public sealed class NoticeController(INoticeService service)
: FreeKitController
{
[HttpPost]
[Authorize(NoticePermissions.Notices.Create)]
public Task<NoticeDto> PublishAsync(
[FromBody] PublishNoticeRequest request)
{
return service.PublishAsync(request);
}
}
路由显式写成 /api/notice/notices。FreeKitController 默认要求登录,Action 再要求 Notice.Notices.Create 权限。
主 Host 当前只在 DEBUG 下执行权限扫描同步;生产环境必须把新权限纳入发布初始化流程,否则动态 policy 会正确拒绝尚未入库的权限。
8. 创建 HttpApi Startup
using IGeekFan.FreeKit.Modularity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace FreeKit.Notice.HttpApi;
public sealed class NoticeHttpApiModuleStartup : IModuleStartup
{
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration)
{
}
public void Configure(
WebApplication app,
IWebHostEnvironment env)
{
}
}
即使当前为空,这个 Startup 也不能省略:AddModule 根据 Startup 所在程序集添加 MVC ApplicationPart,Controller 才会被发现。未来的 Hub、HTTP Client 或 HTTP 专属替换也应放在这里。
9. 接入 FreeKit.DI
FreeKit.DI 必须能编译引用 Core Startup。在 src/Services/Host/FreeKit.DI/FreeKit.DI.csproj 增加:
<ProjectReference Include="..\..\Notice\FreeKit.Notice\FreeKit.Notice.csproj" />
然后修改 src/Services/Host/FreeKit.DI/E.cs:
using FreeKit.Notice;
public static readonly Dictionary<string, Type> Modules =
new(StringComparer.OrdinalIgnoreCase)
{
// 现有模块……
["notice"] = typeof(NoticeModuleStartup)
};
这里登记 Core Startup,使 Job、Message 等后台 Host 可以引用业务服务而不暴露 Controller。
10. 接入主 Host
首先在 src/Services/Host/FreeKit.Host/FreeKit.Host.csproj 引用 HttpApi:
<ProjectReference Include="..\..\Notice\FreeKit.Notice.HttpApi\FreeKit.Notice.HttpApi.csproj" />
然后在 src/Services/Host/FreeKit.Host/Program.cs 增加命名空间:
using FreeKit.Notice;
using FreeKit.Notice.HttpApi;
与其他业务模块一样,先直接注册 Core:
builder.Services.AddModule<NoticeModuleStartup>(
"notice",
builder.Configuration);
再在 hostModules 初始化器中把 Core 替换成 HttpApi:
var hostModules = new Dictionary<string, Type>(
E.Modules,
StringComparer.OrdinalIgnoreCase)
{
// 现有替换……
["notice"] = typeof(NoticeHttpApiModuleStartup)
};
最后把 Core 程序集加入 extraAssemblies:
builder.UseFreeKit(
moduleTypeMap: hostModules,
extraAssemblies:
[
// 现有程序集……
typeof(NoticeModuleStartup).Assembly
]);
不要再调用第二次 UseFreeKit。现有 app.ConfigureModules().Init() 会同时执行 Notice Core 和 HttpApi 的 Configure。
当前 Job 和 Message Host 虽然使用
E.Modules注册 Core,但没有调用ConfigureModules()。如果 Notice 的后台能力依赖Configure初始化,需要先在相应 Host 明确补齐该生命周期,不能假设它已经执行。
11. 构建和验证
dotnet restore FreeKitModules.slnx
dotnet build FreeKitModules.slnx -c Release
dotnet test FreeKitModules.slnx
dotnet run --project src/Services/Host/FreeKit.Host
开发环境打开:
- Swagger:
https://localhost:7000/kit_api/swagger - 接口:
POST https://localhost:7000/kit_api/api/notice/notices
在 Swagger 中先完成认证,并确保当前用户拥有 Notice.Notices.Create。请求示例:
{
"title": "系统维护通知",
"content": "今晚 23:00 开始维护。"
}
接入检查表
- Core 和 HttpApi 都是
net10.0,HttpApi 引用 Core。 - Core/HttpApi Startup 都完整实现
ConfigureServices和Configure。 -
FreeKit.DI.csproj引用 Core,E.Modules登记 Core Startup。 -
FreeKit.Host.csproj引用 HttpApi。 - 主 Host 直接注册 Core,并把同一 key 替换成 HttpApi Startup。
- Core 程序集已加入
extraAssemblies。 - Controller 使用真实 Swagger 分组、显式路由和权限常量。
- Release 有数据库迁移与权限初始化方案。
- 异步方法以
Async结尾,持久化时间使用DateTime.UtcNow。