基础设施与分层
FreeKitModules 使用轻量、务实的 DDD。依赖方向以 Controller → Application Service → Domain Manager/Repository 为主,但仓库保留了若干历史结构,不能把每个模块描述成完全相同的四层模板。
当前项目边界
| 模块 | Core | HTTP 接入 | 结构特点 |
|---|---|---|---|
| CmsKit | FreeKit.CmsKit | FreeKit.CmsKit.HttpApi | Domain 与 Application 按 Content、Engagement、Discovery 等子域组织,最接近标准 DDD |
| Identity | FreeKit.Identity、FreeKit.Identity.Infrastructure | FreeKit.Identity.HttpApi | Infrastructure 同时承载文件、设置、公共附件及其历史 Controller |
| Member | FreeKit.Member | FreeKit.Member.HttpApi | 以 Application、Contracts、Models 为主,当前没有独立 Domain/Manager 目录 |
| Platform | FreeKit.Platform | FreeKit.Platform.HttpApi | Holiday、ShortUrl、ToDo 是垂直切片,其余能力仍有 Application 内混合结构 |
新代码应向清晰边界收敛,但不要为了满足目录模板而移动无关存量代码。
各层职责
HttpApi
HttpApi 项目只处理 HTTP/SignalR:
- Controller 路由、模型绑定和状态码。
[Authorize]、[AllowAnonymous]、权限 policy 或KitAuthorize。- Swagger
GroupName。 - SignalR Hub、Hub Filter 和 HTTP 专属服务替换。
Controller 应依赖 Application Service,而不是注入 IFreeSql 或直接调用 Domain Manager。FreeKitController 只提供 [ApiController] 和默认 [Authorize],不承载业务逻辑。
Application
Application Service 编排一个用户用例:
- 获取当前用户/租户。
- 校验权限和资源归属。
- 调用一个或多个 Manager、Repository 或外部端口。
- 控制事务、事件发布、DTO 映射和返回结果。
需要共享能力时继承 ApplicationService;通用 CRUD 可继承 CrudAppService<...>。CmsKit 的 ArticleService、Identity 的 PermissionService、Platform 的 ToDoService 都是当前源码范本。
Domain
Domain 层包含实体行为和跨实体业务规则。命名约定为:
| 类型 | 接口 | 实现 |
|---|---|---|
| 领域服务 | IXxxManager | XxxManager |
| 应用服务 | IXxxService | XxxService |
领域服务接口继承 IDomainService,实现通常继承 DomainService。例如 IArticleManager / ArticleManager 负责文章可评论性、可见性和互动计数规则。
实体可以把局部不变量封装成行为。实际的 ToDo 实体提供:
public void MarkDone(bool isDone)
{
IsDone = isDone;
DoneTime = isDone ? DateTime.Now : null;
}
public bool CanBeAccessedBy(Guid userId)
{
return CreateUserId == userId;
}
这是存量实现;新写入时间必须使用 DateTime.UtcNow,不要复制这里的 DateTime.Now。
Infrastructure / Data
以下代码可以直接依赖 IFreeSql:
- FreeSql 注册、AOP 和全局过滤器。
- Module Startup 中的 CodeFirst/迁移初始化。
- Repository、DbContext、Schema Migrator 等明确的数据基础设施。
Application 和 Controller 新代码不得直接注入 IFreeSql。复杂查询应封装在审计仓储之上的查询对象、专用 Repository 或 Domain Manager 中。
审计实体
FullAuditEntity 是 FullAuditEntity<Guid,Guid> 的简写;泛型版本的第一个参数是实体主键类型,第二个参数是用户主键类型:
public class KitFile : FullAuditEntity<long, long>, ITenant
{
public Guid? TenantId { get; set; }
public string Name { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
}
它真实包含:
IdCreateUserId、CreateUserName、CreateTimeUpdateUserId、UpdateUserName、UpdateTimeDeleteUserId、DeleteUserName、DeleteTimeIsDeleted
租户实体另外实现 ITenant 并提供 Guid? TenantId。不要把所有实体都假设为 Guid 主键;KitFile、导航等模块使用 long。
仓储边界
FreeKit 提供两个审计仓储接口:
public interface IAuditBaseRepository<TEntity>
: IBaseRepository<TEntity, Guid>
where TEntity : class;
public interface IAuditBaseRepository<TEntity, TKey>
: IBaseRepository<TEntity, TKey>
where TEntity : class;
使用方式与 FreeSql Repository 一致:
ToDo? todo = await todoRepository.Select
.Where(x => x.Id == id)
.FirstAsync();
if (todo is not null)
{
todo.MarkDone(true);
await todoRepository.UpdateAsync(todo);
}
这里的 ToDo 和 todoRepository 对应
FreeKit.ToDos.Application.ToDoService 中的真实实体与
IAuditBaseRepository<ToDo>。
审计行为由 AuditBaseRepository<TEntity,TKey> 在 Insert/Update/Delete 前处理:
- 实现创建审计接口时补充创建人和创建时间。
- 实现更新审计接口时补充修改信息。
- 实现
ISoftDelete或删除审计接口时,DeleteAsync转为更新软删除字段。
只有通过审计仓储或其派生实现写入,才能获得这些行为;直接使用 IFreeSql.Insert/Update/Delete 不会自动经过 Repository 的审计钩子。
FreeSql 全局行为
AddDefaultFreeSql 在 Web BuildingBlock 中统一配置:
- 实体属性名转下划线命名。
ISoftDelete全局过滤。ITenant租户过滤。- 审计、敏感词和 SQL Activity AOP。
UnitOfWorkManager与开放泛型IAuditBaseRepository。
数据库类型由 ConnectionStrings:DefaultDB 解析为 FreeSql DataType,再读取对应连接串。DefaultDB: "0" 表示 MySQL,不是内存数据库。
事务
工作单元拦截器扫描名称以 Service 结尾的公开类。需要显式事务的应用方法使用现有 [Transactional]:
[Transactional]
public override async Task DeleteAsync(Guid id)
{
Article article = await articleRepository.Select
.Where(x => x.Id == id)
.FirstAsync();
await articleRepository.DeleteAsync(article);
await relatedRepository.DeleteAsync(x => x.ArticleId == id);
}
CAP 与数据库事务联合提交时,可通过 ApplicationService.UnitOfWorkManager 和 ICapPublisher 建立 CAP Transaction;范本见 FreeKit.Identity.Application.Profiles.ProfileService.SetAvatarAsync。
Autofac 扫描与显式注册
UseFreeKit 对 moduleTypeMap 和 extraAssemblies 收集到的程序集执行:
UnitOfWorkModule:注册*Service。RegisterDomainServices:注册IDomainService。FreeKitModule:注册三个依赖标记接口。
所以主 Host 必须把 Core 程序集传入 extraAssemblies;只注册 HttpApi Startup 会发现 Controller,却可能漏掉 Core 中的应用服务和领域服务。
需要明确顺序或实现替换的服务仍在 ConfigureServices 中显式注册,不要完全依赖扫描。
已知历史例外
Identity.Infrastructure含文件、设置和公共附件 Controller。- Platform 的部分模型和 Manager 位于
Application子目录。 HolidayService、DailyHotJobService以及少量 CmsKit Application Service 仍直接访问IFreeSql。- Member 当前没有独立 Domain 层。
- 多处存量代码仍使用
DateTime.Now或异步方法缺少Async后缀。
这些是渐进重构边界,不是新代码范本。具体守护规则见 .github/guidelines/conventions.md。
源码索引
src/FreeKit/src/IGeekFan.FreeKit.Extras/AuditEntity/FullAuditEntity.cssrc/FreeKit/src/IGeekFan.FreeKit.Extras/FreeSql/IAuditBaseRepository.cssrc/FreeKit/src/IGeekFan.FreeKit.Extras/FreeSql/AuditBaseRepository.cssrc/BuildingBlocks/IGeekFan.FreeKit.Web/Extensions/ServiceCollectionExtensions.cssrc/BuildingBlocks/IGeekFan.FreeKit.Web/DI/ContainerBuilderExtensions.cssrc/BuildingBlocks/IGeekFan.FreeKit.Infrastructure/Application/AppliactionService.cssrc/BuildingBlocks/IGeekFan.FreeKit.Infrastructure/DDD/DomainService.cs