Identity.Infrastructure 开发指南
Infrastructure 层为 Identity 及所有业务模块提供一组可复用的横切能力,而非某个特定业务功能。它解决的是"文件怎么存、配置怎么读、附件怎么挂、字典怎么管"这类通用问题,任何模块(CmsKit、Platform 等)都可以直接消费这些服务。
该层由 IdentityInfrastructureModuleStartup 独立注册;它不是 IdentityHttpApiModuleStartup 的隐式依赖。Web Host 的完整接入需要同时组合 Identity Core、Infrastructure 与 HttpApi。
本指南聚焦功能与用法,不罗列实体字段。涉及到的实体仅用于帮助理解数据模型。具体 HTTP 接口见 Swagger。
| 能力 | 一句话说明 | 主要服务接口 |
|---|---|---|
| 文件存储 Files | 统一文件上传/下载,支持本地、七牛、CloudFlare R2,按内容哈希秒传去重 | IFileService / IFileManager / IAvatarService |
| 设置系统 Settings | 分层(用户/租户/系统/角色/配置)键值配置,运行时可覆盖 | ISettingManager / ISettingService |
| 设置定义 SettingDefinitions | 配置的元 Schema,驱动前端动态表单 | ISettingDefinitionManager / ISettingDefinitionService |
| 通用附件 CommonAttachments | 把文件挂载到任意业务实体(文章封面、内容图等),带类型与权限校验 | ICommonAttachmentService / ICommonAttachmentManager |
| 数据字典 BaseType / BaseItem | 通用枚举字典(性别、状态码等),前端下拉数据源 | IBaseTypeService / IBaseItemService |
1. 文件存储(Files)
能力概述
文件上传后生成 KitFile 记录(存储路径、大小、后缀、哈希等),并通过 KitFileMeta 按 MD5 内容哈希聚合同一份文件在不同存储后端(本地 / R2 / 七牛)的多条记录,实现秒传去重:相同内容第二次上传不会重复落盘,只新增一条引用。
支持的存储后端由 FileUploadType 枚举描述,通过 IFileStorageOptionsResolver 解析 ServiceName 选择具体实现。当前注册 LocalFileManager、QiniuManager、CloudFlareR2Manager;Sosoos 枚举值仅用于兼容历史文件记录,不再提供上传实现。
核心服务
IFileService:上传与文件 CRUD。UploadAsync有多种重载——IFormFile、字节数组、Stream、远程 URL;也支持批量上传(受NumLimit/MaxFileSize约束)。IFileManager:根据Path或FileMetaId解析可访问的完整Url,支持批量回填、缓存与头像 URL 解析(FillAvatarUrlsAsync/BindFileUrls)。业务表只存Path或FileMetaId,展示时统一通过它取 URL。IAvatarService:基于 SkiaSharp 生成文字头像(GenerateAvatar)、按名字复用同一头像(CreateAvatarUrlByNameAsync,MD5 命名避免重复)、随机头像(GetRandomAvatar)。
开发示例
// 1) 上传(例如 TTS 音频,按业务前缀隔离)
var file = await _fileService.UploadAsync(audioBytes, "speech.wav", key: 0, prefixPath: "tts");
// file.Path / file.Url / file.Id 可存入业务表
// 2) 展示时解析完整 URL
var url = await _fileManager.GetFileUrlAsync(file.Path);
配置(FileStorage 节)
{
"FileStorage": {
"MaxFileSize": 10485760,
"NumLimit": 10,
"Include": "jpg,jpeg,png,gif,mp4,mp3,pdf",
"Exclude": "exe,bat",
"ServiceName": "CloudFlareR2",
"LocalFile": { "RootPath": "wwwroot/files", "PrefixPath": "files", "Host": "" },
"CloudflareR2": {
"AccountId": "<R2_ACCOUNT_ID>",
"AccessKeyId": "<R2_ACCESS_KEY_ID>",
"Secret": "<R2_SECRET>",
"ApiToken": "<R2_API_TOKEN>",
"ApiBaseUri": "https://api.cloudflare.com/client/v4",
"Bucket": "<R2_BUCKET>",
"PrefixPath": "files",
"Host": "https://cdn.example.com"
}
}
}
运行时覆盖:
FileStorageOptionsResolver会合并Setting(键FileStorage.Config,JSON)中的值到上述配置,变更 R2 凭据时自动刷新 DI 中的单例客户端。即无需改 appsettings 即可热更新存储配置。
2. 设置系统(Settings)
分层 Provider 模型
设置值(Setting)按来源 Provider分层,取值优先级从高到低为:
用户 (U) > 租户 (T) > 系统 (S) > 配置文件 (C)
全部未命中时回落到 SettingDefinition.DefaultValue。各层由对应的 SettingValueProvider 实现:
UserSettingValueProvider(U,按ProviderKey=用户Id)TenantSettingValueProvider(T)SystemSettingValueProvider(S)ConfigurationSettingValueProvider(C,读IConfiguration,键名.自动转:)
ISettingManager 按优先级聚合取值,启动时还会把 appsettings 中已定义但库中缺失的 S 级配置种子化(标记 BuiltInParams,禁止删除/改键)。
核心服务
ISettingManager:读配置的主力。GetOrNullAsync(name)/GetOrNullAsync<T>()、GetSystemBoolAsync/GetSystemIntAsync/GetSystemStringListAsync、IsTrueAsync、FindAsync。ISettingService:写配置与 CRUD。SetKeyValuesAsync不存在则建、存在则更新(JSON 类型会做语法与字段校验),并保护内置参数不被覆盖。
开发示例
// 读取系统级配置(类型安全)
var host = await _settingManager.GetOrNullAsync("MailKitOptions.Host", SettingProviderNameEnum.S);
var captchaOn = await _settingManager.GetSystemBoolAsync("Captcha.Enabled");
// 写入系统级配置
await _settingService.SetKeyValuesAsync(new SettingCreateUpdateReq
{
Name = "MailKitOptions.Host",
Value = "smtp.example.com",
ProviderName = SettingProviderNameEnum.S
});
常用定义名见
Runtime/IdentitySettingNames.cs,如Captcha.Enabled、MailKitOptions.Host、FileStorage.Config、MeiliSearch.Host、RateLimit.Email.*等。读取时把键中的.替换为:查IConfiguration。
3. 设置定义(SettingDefinitions)
定义 vs 值
SettingDefinition= 配置的元 Schema:名称、默认值、数据类型、分组、是否加密、是否对客户端可见、JSON Schema。Setting= 某个 Provider 下的实际值。
这与 ABP 的 Setting 体系一致:定义描述"有哪些可配置项、长什么样",值记录"当前配了什么"。
注册定义
定义通过代码中的 ISettingDefinitionProvider 声明,首次启动同步入库(增量同步,不覆盖你在后台维护的展示文案);若 DataType=json 且含 JsonSchemaType,会自动从 CLR 类型递归生成 JSON Schema。
public class MySettingDefinitionProvider : ISettingDefinitionProvider
{
public void Define(ISettingDefinitionContext context)
{
context.Add(new SettingDefinition("My.Feature.Enabled", "false", "功能开关")
.WithDataType("bool")
.WithGroupName("高级")
.WithVisibility(true));
}
}
// 宿主启动时通过反射自动注册所有 ISettingDefinitionProvider
内置实现 IdentitySettingDefinitionProvider 已定义 OAuth、验证码、邮件、MeiliSearch、文件存储、限流等全部系统设置。
前端动态表单
拉取可见定义即可按 DataType / Options / GroupName 自动渲染输入控件(无需手工维护表单配置)。
4. 通用附件(CommonAttachments)
能力概述
通用附件把"文件"挂载到任意业务实体上(如文章封面、文章内容图、视频等)。它不存储文件本身,只存 FilePath / FileMetaId,文件实体仍在 KitFile 中——因此与 Files 能力完全打通:查询附件时自动回填 FileUrl。附件类型由扩展名推断(Image/Video/Audio/Document/Other)。
配置实体类型与访问校验
消费方需在模块启动时声明"哪些实体类型允许挂附件",以及分类规则与权限校验器:
services.Configure<CommonAttachmentOptions>(o =>
{
o.EntityTypes.Add(new CommonFileEntityTypeDefinition(nameof(Article))
.WithAccessValidator<ArticleAccessValidator>() // 上传前校验实体归属权限
.WithCategory("Article/Cover", AttachmentType.Image) // 封面:仅图片
.WithCategory("Article/Content", // 内容:多种类型
AttachmentType.Image, AttachmentType.Video,
AttachmentType.Audio, AttachmentType.Document, AttachmentType.Other));
});
上传/查询会经过 ICommonAttachmentValidator:校验实体类型是否允许挂附件、当前用户是否有权操作该实体,非法时抛 BusinessException / EntityCantHaveException。
典型使用流程
- 业务实体(如
Article)要支持附件时,在模块启动里Configure<CommonAttachmentOptions>注册CommonFileEntityTypeDefinition与访问校验器。 - 先走 Files 上传拿到
Path,再以代码方式挂载附件:
// 先上传拿到 Path
var file = await _fileService.UploadAsync(bytes, "cover.png", prefixPath: "article");
// 再挂为某实体的封面附件
await _commonAttachmentService.CreateAsync(new AttachmentCreateInput
{
EntityId = articleId,
EntityType = nameof(Article),
Category = "Article/Cover",
AttachmentType = AttachmentType.Image,
FilePath = file.Path,
FileName = "cover.png",
FileSize = file.Size
});
// 查询时自动回填 FileUrl
var attachments = await _commonAttachmentService.GetListByEntityAsync(articleId);
5. 数据字典(BaseType / BaseItem)
概念
通用数据字典,用于替代散落的硬编码枚举。关系为父子:
BaseType:字典分类(如"性别"),含唯一TypeCode。BaseItem:字典项(如"男/女"),含ItemCode/ItemName,可自引用形成层级,并支持ExtraProperties(JSON 扩展字典)。
开发者通常把 BaseType 当作"枚举组"、BaseItem 当作"枚举值",并通过 TypeCode 查询(自动转换为 BaseTypeId)。
前端下拉用法
下拉数据源通过 BaseItem 的 select 接口获取,返回 Label=ItemName, Value=ItemCode,前端直接用 ItemCode 作为业务值。
模块注册要点
IdentityInfrastructureModuleStartup 在启动时完成:
- 绑定
FileStorageOption与CloudflareR2Options配置; - 以 keyed 方式注册 3 个文件上传实现(
LocalFileManager/QiniuManager/CloudFlareR2Manager); - 反射注册所有
ISettingDefinitionProvider,并注册ISettingDefinitionManager单例; - 注册各层
SettingValueProvider(Configuration 单例,User/Tenant/System 为 Scoped); - 启动时
SettingDefinitionManager.Initialize(...)同步定义,ISettingManager.InitializeFromConfigurationAsync()把 appsettings 种子化为系统设置。
在自定义 Host 中,显式注册这一层:
builder.Services.AddModule<IdentityModuleStartup>(
"identity",
builder.Configuration);
builder.Services.AddModule<IdentityInfrastructureModuleStartup>(
"identity-inf",
builder.Configuration);
Web API 还需注册 IdentityHttpApiModuleStartup;仓库主 Host 通过 E.Modules 携带 identity-inf,并用 HttpApi 启动类替换 identity 映射。不要重复注册同一个启动类,完整写法见 Identity 模块索引。
:::warning 生产初始化
IdentityInfrastructureModuleStartup.Configure 中的 KitFileMeta、CommonAttachment、SettingDefinition、Setting 等 SyncStructure(...) 受 #if DEBUG 保护。生产环境应使用受控迁移,不能依赖 Debug 自动建表。
:::
源码定位
src/Services/Identity/FreeKit.Identity.Infrastructure/IdentityInfrastructureModuleStartup.cssrc/Services/Identity/FreeKit.Identity.Infrastructure/Domain/Files/FileStorageOptionsResolver.cssrc/Services/Identity/FreeKit.Identity.Infrastructure/Domain/Settings/SettingManager.cssrc/Services/Host/FreeKit.DI/E.cs