IGeekFan.FreeKit.Extras
源码位置:
src/FreeKit/src/IGeekFan.FreeKit.Extras目标框架:net8.0/net10.0(启用Nullable、ImplicitUsings,并生成 XML 文档)
IGeekFan.FreeKit.Extras 是 FreeKit 框架在 IGeekFan.FreeKit(核心)之上的应用基础设施扩展包。它在 FreeSql 之上封装了"开箱即用"的 ASP.NET Core 能力,让业务模块无需重复编写样板代码即可获得:仓储与审计字段自动填充、声明式事务(工作单元)、当前登录人解析、多租户、大小写命名转换、分页 DTO、Autofac 约定式自动注册等。
包的整体用途与设计目标
| 设计目标 | 说明 |
|---|---|
| 降低 FreeSql 使用成本 | 提供泛型仓储(IBaseRepository、审计仓储、复合主键仓储),自动注入,无需手写 new。 |
| 审计字段零侵入 | 实体只要实现审计接口(ICreateAuditEntity 等),插入/更新/删除时由仓储自动填入操作人、时间、租户。 |
| 声明式事务 | 用 [Transactional] 标注方法或服务即可开启工作单元;支持传播行为、隔离级别、领域事件自动发布。 |
| 当前用户随处可取得 | 通过 ICurrentUser / ICurrentUserAccessor 在任意层获取登录人信息,并驱动审计填充。 |
| 多租户开箱即用 | 提供请求头 / Query / Claim / Host 多种租户解析器 + FreeSql 全局过滤,一行配置接入。 |
| 前后端命名风格解耦 | 提供 Snake / Camel / Lower 三套查询参数与 API 描述改写,后端 PascalCase、前端下划线。 |
| 约定式 DI | 实现 IScopedDependency / ITransientDependency / ISingletonDependency 的组件自动注册;*Service 自动接入事务拦截。 |
它与 IGeekFan.FreeKit(定义实体/审计接口、依赖标记、领域事件抽象、模块系统)是紧耦合但分层的关系:Extras 是"在 Web / FreeSql 场景下对这些契约的具体实现与装配"。
核心模块与类一览
| 命名空间 | 模块 | 关键类型 |
|---|---|---|
IGeekFan.FreeKit.Extras.Security | 当前用户 | ICurrentUser / CurrentUser、ICurrentUserAccessor / CurrentUserAccessor、CurrentUserAccessorMiddleware、ClaimsPrincipalExtensions、CurrentUserExtensions、FreeKitClaimTypes、UserAccessToken |
IGeekFan.FreeKit.Extras.FreeSql | 仓储与事务 | IAuditBaseRepository<>、AuditBaseRepository<,>、AuditDefaultRepository<,,>、AuditGuidRepository<> 等、ICompositeRepository<,,>、CompositeDefaultRepository<,,>、FreeSqlExtension、ReflexHelper、TransactionalAttribute、UnitOfWorkActionFilter、UnitOfWorkInterceptor、UnitOfWorkAsyncInterceptor、UnitOfWorkDefaultOptions |
FreeSql(非常规命名空间) | FreeSql 辅助 | FreeSqlExtension、FreeSql.Aop.AuditValueEventArgsExtension |
IGeekFan.FreeKit.Extras.MultiTenancy | 多租户 | MultiTenancyOptions、AddMultiTenancy / UseMultiTenancy、ITenantResolver(Claim/Header/QueryString/Host)、TenantInfo、ITenantStore、ITenantAccessor / TenantAccessor、TenantMiddleware、IgnoreTenantAttribute、FreeSqlMultiTenancyExtensions |
IGeekFan.FreeKit.Extras.CaseQuery | 命名风格转换 | SnakeApiDescriptionProvider / Lower / CamelCase、SnakeCaseQueryValueProvider 等、SnakeCaseValueProviderFactory 等 |
IGeekFan.FreeKit.Extras.Dto | 分页 DTO | PagedResultDto<T> |
IGeekFan.FreeKit.Extras.Extensions | 通用扩展 | DateTimeExtensions、StringExtensions、EnumerableExtensions、TypeExtensions |
IGeekFan.FreeKit.Extras.AuditEntity | 实体基类 | Entity / Entity<T>、FullAuditEntity / FullAuditEntity<T,U>、CreateAuditEntity |
IGeekFan.FreeKit.Extras.Dependency | Autofac 模块 | FreeKitModule、UnitOfWorkModule |
Microsoft.Extensions.DependencyInjection | 服务注册 | ServiceCollectionExtensions(AddFreeKitCore、AddUnitOfWorkManager 等) |
安装与依赖
dotnet add package IGeekFan.FreeKit.Extras
包级依赖(IGeekFan.FreeKit.Extras.csproj):
| 依赖 | 用途 |
|---|---|
IGeekFan.FreeKit(ProjectReference) | 实体/审计接口、依赖标记、领域事件抽象、模块系统 |
FreeSql.DbContext | ORM 与仓储基类(DefaultRepository、UnitOfWorkManager 等) |
Autofac.Extensions.DependencyInjection 10.0.0 | Autofac 容器与 FreeKitModule / UnitOfWorkModule |
Autofac.Extras.DynamicProxy 7.1.0 | 动态代理(事务/事件拦截) |
Castle.Core.AsyncInterceptor 2.1.0 | 异步方法拦截器 |
Serilog.Extensions.Hosting 10.0.0 | 日志(解析失败等告警) |
Microsoft.AspNetCore.App(FrameworkReference) | ASP.NET Core 宿主能力 |
使用 Extras 的多租户与 DI 自动注册通常需要配合 Autofac 作为 ServiceProvider(UseServiceProviderFactory(new AutofacServiceProviderFactory()))。纯 IServiceCollection 也能完成核心注册(AddFreeKitCore / AddUnitOfWorkManager),但约定式自动注册依赖 Autofac 模块。
快速开始
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. 注册 FreeSql 与全局软删除过滤
builder.Services.AddSingleton<IFreeSql>(sp =>
{
var fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, "Data Source=app.db")
.UseAutoSyncStructure(true)
.Build();
fsql.GlobalFilter.Apply<ISoftDelete>("IsDeleted", a => a.IsDeleted == false);
return fsql;
});
// 2. 核心聚合注册:当前用户、访问器、审计仓储、复合/默认仓储
builder.Services.AddFreeKitCore(); // 默认用户主键为 Guid
// builder.Services.AddFreeKitCore(typeof(long)); // 若用户主键为 long
builder.Services.AddUnitOfWorkManager();
// 3. MVC 接入事务过滤器(使 [Transactional] 在 Controller Action 上生效)
builder.Services.AddControllers(options =>
{
options.Filters.AddService(typeof(UnitOfWorkActionFilter));
});
// 4. Autofac 自动注册(约定式 + 事务拦截)
builder.Host
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>((_, cb) =>
{
cb.RegisterModule(new FreeKitModule([typeof(Program).Assembly]));
cb.RegisterModule(new UnitOfWorkModule([typeof(Program).Assembly]));
});
var app = builder.Build();
// 5. 中间件顺序(重要):认证 → 当前用户 → 授权 → 多租户
app.UseAuthentication();
app.UseCurrentUserAccessor();
app.UseAuthorization();
app.UseMultiTenancy();
app.MapControllers();
app.Run();
当前用户(CurrentUser)
ICurrentUser 接口与属性
ICurrentUser(IGeekFan.FreeKit.Extras.Security)从 HttpContext.User 的 Claims 中解析登录人信息。泛型版本 ICurrentUser<T>(T : IEquatable<T>)用于指定用户主键类型,ICurrentUser 即 ICurrentUser<string>。
| 成员 | 类型 | Claim 来源(FreeKitClaimTypes) |
|---|---|---|
IsAuthenticated | bool | 是否存在用户 Id |
Id | T? | NameIdentifier(用户 Id) |
UserName | string? | ClaimTypes.Name(登录名) |
Email | string? | ClaimTypes.Email |
Roles | string[] | ClaimTypes.Role |
TenantId | Guid? | tenantid |
TenantName | string? | tenantname |
FindClaim(string) / FindClaims(string) / GetAllClaims() | — | 按声明类型取 Claim |
IsInRole(string) | bool | 是否拥有某角色 |
FreeKitClaimTypes 常量:TenantName="tenantname"、TenantId="tenantid"、NickName="nickname"、Name=ClaimTypes.GivenName(姓名)、PhoneNumber=ClaimTypes.MobilePhone、UserName=ClaimTypes.Name、Email=ClaimTypes.Email、Role=ClaimTypes.Role、NameIdentifier=ClaimTypes.NameIdentifier(用户 Id)。
CurrentUserAccessor 与中间件
ICurrentUserAccessor/CurrentUserAccessor:基于AsyncLocal的单例,在任意异步上下文中都能访问当前用户(CurrentUserAccessor.CurrentUser),不依赖HttpContext。CurrentUserAccessorMiddleware:通过app.UseCurrentUserAccessor()注册。每个请求进入时把ICurrentUser实例写入访问器,管道结束后清空,避免上下文污染。
public class ArticleService : IScopedDependency
{
private readonly ICurrentUser _currentUser;
public ArticleService(ICurrentUser currentUser) => _currentUser = currentUser;
public async Task CreateAsync(string title)
{
var article = new Article { Title = title };
// 若 Article 实现 ICreateAuditEntity<T>,CreateUserId / CreateTime / CreateUserName 由审计仓储自动填充
await _repository.InsertAsync(article);
}
}
扩展方法
ClaimsPrincipalExtensions:FindUserId()、FindUserIdToGuid()、FindUserIdToLong()、FindUserIdToInt()、FindTenantId()、FindUserName()等,从ClaimsPrincipal按FreeKitClaimTypes取值。CurrentUserExtensions:FindUserId<T>()、FindUserIdToGuid/Long/Int()、FindName()、FindNickName()、FindPhoneNumber()。CurrentUserMultiTenancyExtensions:GetCurrentTenant(ITenantAccessor)取当前租户信息。
UserAccessToken
[Serializable]
public class UserAccessToken
{
public UserAccessToken(string accessToken, string refreshToken, int expiresIn, string tokenType, int refreshExpiresIn);
public string AccessToken { get; } // 授权调用凭证
public string RefreshToken { get; } // 刷新凭证
public int ExpiresIn { get; } // 过期秒数
public string TokenType { get; }
public int RefreshExpiresIn { get; }
}
可序列化的 OAuth 风格令牌封装,属性只读(仅由构造函数赋值)。适合在登录/授权流程中作为统一返回体。
FreeSql 仓储
审计仓储(自动填充审计字段)
审计仓储接口基于 FreeSql 的 IBaseRepository<TEntity, TKey>:
public interface IAuditBaseRepository<TEntity> : IBaseRepository<TEntity, Guid> where TEntity : class { }
public interface IAuditBaseRepository<TEntity, TKey> : IBaseRepository<TEntity, TKey> where TEntity : class { }
AuditBaseRepository<TEntity, TKey> 是抽象基类,重写了 Insert/Update/Delete/InsertOrUpdate 及其异步版本,在前后调用三个钩子:
protected abstract void BeforeInsert(TEntity entity);
protected abstract void BeforeUpdate(TEntity entity);
protected abstract void BeforeDelete(TEntity entity);
AuditDefaultRepository<TEntity, TKey, TUkey> 提供了默认实现(TUkey 为用户主键类型):
- BeforeInsert:若实体实现
ITenant且TenantId为空,写入CurrentUser.TenantId;若实现ICreateAuditEntity<TUkey>,仅当字段为空时写入CreateTime=DateTime.Now、CreateUserId=CurrentUser.FindUserId<TUkey>()、CreateUserName=CurrentUser.UserName。 - BeforeUpdate:若
IUpdateAuditEntity<TUkey>,写入UpdateTime / UpdateUserName / UpdateUserId。 - BeforeDelete:若实体为
ISoftDelete,置IsDeleted=true(软删除);若IDeleteAuditEntity<TUkey>,写入DeleteUserId / DeleteUserName / DeleteTime。
具体实现(依据"表主键 / 用户主键"组合自动选择):
| 类型 | 表主键 | 用户主键 | 注册为 |
|---|---|---|---|
AuditGuidRepository<TEntity> | Guid | Guid | IAuditBaseRepository<TEntity> |
AuditLongRepository<TEntity> | Guid | long | IAuditBaseRepository<TEntity> |
AuditIntRepository<TEntity> | Guid | long(int 按 long 承载) | IAuditBaseRepository<TEntity> |
AuditTKeyGuidRepository<TEntity, TKey> | 自定义 TKey | Guid | IAuditBaseRepository<TEntity, TKey> |
AuditTKeyLongRepository<TEntity, TKey> | 自定义 TKey | long | IAuditBaseRepository<TEntity, TKey> |
AuditTKeyIntRepository<TEntity, TKey> | 自定义 TKey | long | IAuditBaseRepository<TEntity, TKey> |
选择哪一种由
AddFreeKitCore(typeUserkey)决定(见"服务集合扩展"一节)。
默认仓储与复合主键仓储
- 默认仓储:
AddDefaultRepository()注入IBaseRepository<>→GuidRepository<>、IBaseRepository<,>→DefaultRepository<,>(FreeSql 标准仓储,无审计填充)。 - 复合主键仓储:
ICompositeRepository<TEntity, TKey, Ukey>/CompositeRepository<TEntity, TKey, Ukey>/CompositeDefaultRepository<TEntity, TKey, Ukey>。用"两个键"组合定位/删除实体:
public interface ICompositeRepository<TEntity, TKey, Ukey> : IBaseRepository<TEntity>
{
TEntity Get(TKey id, Ukey uid);
int Delete(TKey id, Ukey uid);
Task<TEntity> GetAsync(TKey id, Ukey uid, CancellationToken cancellationToken = default);
Task<int> DeleteAsync(TKey id, Ukey uid, CancellationToken cancellationToken = default);
}
CompositeDefaultRepository 提供三个构造函数:(IFreeSql)、(IFreeSql, Expression<Func<TEntity,bool>> filter)、(IFreeSql, UnitOfWorkManager)(绑定工作单元)。会校验实体必须为双主键且类型匹配。
FreeSqlExtension 辅助
命名空间为 FreeSql(非 IGeekFan.FreeKit.Extras.FreeSql,便于直接 using FreeSql):
| 方法 | 说明 |
|---|---|
UseConnectionString(this FreeSqlBuilder, IConfiguration, prefix="ConnectionStrings") | 从配置读取连接串(默认节 ConnectionStrings,子键 DefaultDB/ProviderType/{dataType}) |
GetConnectionString(this FreeSqlBuilder) | 反射读取已配置的主库连接串 |
AsTable<T>(this ISelect<T>, string tableName, int count) / AsTable<T>(params string[]) | 分表查询(tableName_0..count-1 或指定表名数组) |
AsNoTracking<T>(this ISelect<T>) | 关闭跟踪以提升查询性能 |
AddIfNotContains<T>(this ICollection<T>, T value) | 不存在时才添加 |
FreeSql.Aop.AuditValueEventArgsExtension.AuditValue<T>(this AuditValueEventArgs e, ICurrentUser? user):在 FreeSql 的 AuditValue AOP 事件中,根据列名自动填充 CreateUserId/CreateUserName/CreateTime/TenantId(Insert)与 UpdateUserId/UpdateUserName/UpdateTime(Update)。
事务与工作单元(Transactional)
[Transactional] 特性
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class TransactionalAttribute : Attribute
{
public TransactionalAttribute();
public TransactionalAttribute(IsolationLevel? isolationLevel);
public TransactionalAttribute(bool isDisabled);
public TransactionalAttribute(Propagation propagation);
public TransactionalAttribute(Propagation propagation, IsolationLevel isolationLevel);
public TransactionalAttribute(Propagation propagation, IsolationLevel isolationLevel, bool isDisabled);
public Propagation? Propagation { get; set; } // null 时回退到 UnitOfWorkDefaultOptions
public IsolationLevel? IsolationLevel { get; set; }
public bool IsDisabled { get; set; } // 默认 false
}
| 用法 | 说明 |
|---|---|
[Transactional] | 默认新事务(Propagation=Required) |
[Transactional(IsDisabled = true)] | 禁用事务(方法内不开启 UoW) |
[Transactional(Propagation = Propagation.RequiresNew)] | 总是开启一个新事务 |
[Transactional(IsolationLevel = IsolationLevel.ReadCommitted)] | 指定隔离级别 |
Propagation 取 FreeSql 的 Propagation 枚举(常用:Required 默认、RequiresNew 总是新事务、Suppress 不使用事务等);IsolationLevel 为 System.Data.IsolationLevel。
两种生效路径:
- 服务层拦截(推荐):
UnitOfWorkModule(Autofac)为所有以Service结尾的类启用UnitOfWorkInterceptor/UnitOfWorkAsyncInterceptor接口/类拦截。被[Transactional]标注的方法在调用时自动开启工作单元,成功Commit、异常Rollback。 - MVC Action 过滤器:
UnitOfWorkActionFilter检测 Controller/Action 上的[Transactional],开启UnitOfWorkManager.Begin(...),Action 结束后提交/回滚(需在AddControllers中Filters.AddService(typeof(UnitOfWorkActionFilter)))。
配置项(UnitOfWorkDefaultOptions)
public class UnitOfWorkDefaultOptions
{
public Propagation? Propagation { get; set; } // null → Required
public IsolationLevel? IsolationLevel { get; set; }
public bool PublishDomainEvent { get; set; } = true; // 是否发布领域事件
}
通过 services.Configure<UnitOfWorkDefaultOptions>(o => { ... }) 配置全局默认行为。
领域事件自动发布
UnitOfWorkAsyncInterceptor 在事务提交前,会扫描 IUnitOfWork.EntityChangeReport 中实现了 IDomainEventBase(来自 IGeekFan.FreeKit.Extras.Domain)且携带领域事件的实体,经 IMediator.Publish 自动发布。该机制将"仓储变更"与"领域副作用"(发邮件、写日志、更新缓存等)解耦,递归发布上限为 10 层(超出抛内部 RecursionOverflowException)。
// 实体聚合根实现 IDomainEventBase,即可被自动发布
public class Order : FullAuditEntity, IDomainEventBase
{
public List<IDomainEvent> DomainEvents { get; set; } = new();
public void Ship() => AddDomainEvent(new OrderShippedEvent(Id));
}
场景示例:多表原子操作
[Transactional]
public async Task TransferAsync(Guid fromId, Guid toId, decimal amount)
{
var from = await _repo.FindAsync(fromId);
var to = await _repo.FindAsync(toId);
from.Balance -= amount;
to.Balance += amount;
await _repo.UpdateAsync(from);
await _repo.UpdateAsync(to);
// 异常自动回滚
}
多租户(MultiTenancy)
提供从请求中解析租户 → 写入 ITenantAccessor → FreeSql 全局过滤的完整链路。
配置项(MultiTenancyOptions)
public class MultiTenancyOptions
{
public bool IsEnabled { get; set; } = true;
public bool EnableHeaderResolver { get; set; } = true;
public string HeaderName { get; set; } = "X-Tenant-Id";
public bool EnableQueryStringResolver { get; set; } = true;
public string QueryStringKey { get; set; } = "tenantId";
public bool EnableClaimResolver { get; set; } = true;
public bool EnableHostResolver { get; set; } = false; // 默认关闭
public string? TenantDomainSuffix { get; set; }
public bool UseSubdomain { get; set; } = true;
public bool EnableGlobalQueryFilter { get; set; } = true;
}
租户解析器与优先级
AddMultiTenancy(configure) 按选项开关向 DI 注册对应的 ITenantResolver(Transient)。TenantMiddleware 在请求时按 Priority 降序依次尝试,第一个非空结果即当前租户并写入 TenantAccessor;若端点/控制器/方法标注 [IgnoreTenant] 则完全跳过解析。
| 解析器 | Priority | 来源 | 默认是否启用 | 行为 |
|---|---|---|---|---|
HostTenantResolver | 40 | 请求 Host | 否(EnableHostResolver=false) | 配置了 TenantDomainSuffix 时取子域为 Code;否则 UseSubdomain=true 且 Host ≥ 3 段时取第一段为 Code |
QueryStringTenantResolver | 30 | Query 参数 tenantId | 是 | Guid 解析为 Id,否则作为 Code |
HeaderTenantResolver | 20 | 请求头 X-Tenant-Id | 是 | 同上 |
ClaimTenantResolver | 10 | 已认证用户 Claim | 是 | 取 FreeKitClaimTypes.TenantId(Guid) 与 TenantName;未认证或无租户返回 null |
builder.Services.AddMultiTenancy(options =>
{
options.EnableHostResolver = true;
options.TenantDomainSuffix = ".myapp.com";
options.HeaderName = "X-Tenant";
});
// 管道中:app.UseMultiTenancy();
TenantInfo / ITenantStore / ITenantAccessor
public class TenantInfo
{
public Guid? Id { get; set; }
public string? Name { get; set; }
public string? Code { get; set; }
public string? ConnectionString { get; set; }
public bool IsEnabled { get; set; } = true;
public static TenantInfo Default => new() { Id = null, Name = "Default", Code = "default" };
}
public interface ITenantAccessor
{
TenantInfo? Tenant { get; set; }
Guid? TenantId => Tenant?.Id;
string? TenantName => Tenant?.Name;
bool IsTenantResolved => Tenant != null;
}
public interface ITenantStore : ITransientDependency // 由使用者自行实现租户存储
{
Task<TenantInfo?> FindByIdAsync(Guid tenantId);
Task<TenantInfo?> FindByNameAsync(string tenantName);
Task<TenantInfo?> FindByCodeAsync(string tenantCode);
}
TenantAccessor 基于 AsyncLocal<TenantInfo?>,并暴露静态 TenantAccessor.CurrentTenant 快捷访问。若启用全局查询过滤,需在 FreeSql 配置里实现 ITenantStore 并注册,或在解析器阶段已足以根据 Code 取得 TenantInfo。
中间件与 [IgnoreTenant]
// 整个 Controller 或 Action 忽略租户解析与过滤
[IgnoreTenant]
public class PublicController : ControllerBase { }
IgnoreTenantFilter(Scoped)在标注了 [IgnoreTenant] 的 Action 执行期间调用 BeginIgnoreTenant(),临时关闭租户过滤。
FreeSql 多租户过滤器
// 注册全局过滤器(默认从 TenantAccessor.CurrentTenant?.Id 取当前租户)
fsql.UseMultiTenancyFilter();
// 或自定义租户来源
fsql.UseMultiTenancyFilter(() => GetCurrentTenantId());
// 单次查询绕过过滤
var list = fsql.Select<TenantEntity>().IgnoreTenantFilter().ToList();
// 代码块级临时关闭
using (FreeSqlMultiTenancyExtensions.BeginIgnoreTenant())
{
var all = fsql.Select<TenantEntity>().ToList();
}
全局过滤逻辑:t.TenantId == 当前租户 || 当前租户为空 || 过滤被禁用。EnableGlobalQueryFilter=false 时不注册该过滤。
异常
MultiTenancyException 提供工厂方法:TenantNotFound(Guid)、TenantNotResolved()、TenantDisabled(Guid)。
命名风格转换(CaseQuery)
后端参数为 PascalCase 时,前端可用 snake_case / camelCase / 全小写调用。提供三套组件(Snake / Lower / CamelCase):
XxxApiDescriptionProvider(IApiDescriptionProvider,Order=1):改写 Swagger / API 描述中的参数名为对应风格。XxxCaseQueryValueProvider(QueryStringValueProvider):重写查询参数绑定时的 key 大小写。XxxCaseValueProviderFactory(IValueProviderFactory):向 MVC 注册对应的 ValueProvider。
// Program.cs
services.TryAddEnumerable(ServiceDescriptor.Transient<IApiDescriptionProvider, SnakeApiDescriptionProvider>());
services.AddControllers(options =>
{
options.ValueProviderFactories.Add(new SnakeCaseValueProviderFactory());
options.Filters.AddService(typeof(UnitOfWorkActionFilter));
});
samples/IGeekFan.FreeKit.Web 中使用的是 CamelCaseApiDescriptionProvider(配合 RouteOptions.LowercaseUrls/LowercaseQueryStrings)。按团队前端约定选择对应风格即可。
分页 DTO(PagedResultDto)
public class PagedResultDto<T> : BasePagingInfo where T : class
{
public IReadOnlyList<T> Items { get; set; }
public PagedResultDto();
public PagedResultDto(IReadOnlyList<T> items); // count = items.Count
public PagedResultDto(IReadOnlyList<T> items, long count);
public PagedResultDto(IReadOnlyList<T> items, BasePagingInfo page);
public PagedResultDto(IReadOnlyList<T> items, long count, int pageNumber, int pageSize);
}
BasePagingInfo(来自 FreeSql.Internal.Model)携带 Count / PageNumber / PageSize 等分页元数据,用于统一分页返回结构。应用层可基于 FreeSql 的 ToPageList 结果直接构造:
var (items, total) = await _repo.Select.Page(maxPerPage, pageNumber).ToListAsync(a => new ArticleDto());
return new PagedResultDto<ArticleDto>(items, total);
通用扩展方法(Extensions)
命名空间 IGeekFan.FreeKit.Extras.Extensions:
| 类 | 主要方法 | 说明 |
|---|---|---|
DateTimeExtensions | ToDateTimeString(removeSecond=false)、ToDateString()、ToTimeString()、ToMillisecondString()、ToChineseDateString()、ToChineseDateTimeString()、Description(this TimeSpan) | 各类日期/时间格式化与中文格式 |
StringExtensions | ToLong()、IsNullOrEmpty/IsNotNullOrEmpty/IsNullOrWhiteSpace/IsNotNullOrWhiteSpace、ToPascalCase()、ToCamelCase()、ToKebabCase()、ToSnakeCase()、ToTrainCase() | 判空与大小写风格转换(被 CaseQuery 复用) |
EnumerableExtensions | LoopIndex<T>(this IEnumerable<T>) | 返回 (item, index) 序列 |
TypeExtensions | HasImplementedRawGeneric(this Type, Type generic) | 判断类型是否实现某未关闭泛型接口(审计仓储用于识别 ISoftDelete / IDeleteAuditEntity<> / IUpdateAuditEntity<> 等) |
审计实体基类(AuditEntity)
命名空间 IGeekFan.FreeKit.Extras.AuditEntity(接口定义于 IGeekFan.FreeKit,此处提供基类实现):
public abstract class Entity<T> : IEntity<T> where T : IEquatable<T>
{
[Column(IsPrimary = true, IsIdentity = true, Position = 1)]
[Required] public virtual T Id { get; set; }
public virtual object[] GetKeys() => new object[] { Id };
}
public abstract class Entity : Entity<Guid> { }
// 创建审计(用户主键类型 U)
public class CreateAuditEntity<TKey, TUKey> : Entity<TKey>, ICreateAuditEntity<TUKey> { ... }
public class CreateAuditEntity : CreateAuditEntity<Guid, Guid>, ICreateAuditEntity { }
// 完整审计(创建 + 修改 + 删除 + 软删除)
[Serializable]
public class FullAuditEntity<T, U> : Entity<T>,
ICreateAuditEntity<U>, IUpdateAuditEntity<U>, IDeleteAuditEntity<U> { ... }
public class FullAuditEntity : FullAuditEntity<Guid, Guid>, IFullAuditEntity<Guid, Guid> { }
FullAuditEntity<T, U> 共 10 个字段:Id(来自 Entity)+ 创建三人组(CreateUserId/CreateUserName/CreateTime)+ 修改三人组(UpdateUserId/UpdateUserName/UpdateTime)+ 删除三人组(DeleteUserId/DeleteUserName/DeleteTime)+ IsDeleted。其中 T 为表主键类型,U 为用户主键类型。
依赖的审计接口(定义于 IGeekFan.FreeKit.Extras.AuditEntity):IEntity<T>、ICreateAuditEntity<T>、IUpdateAuditEntity<T>、IDeleteAuditEntity<T>(继承 ISoftDelete)、IFullAuditEntity<TKey,UKey>、ISoftDelete、ITenant。
依赖注入自动注册(Dependency 模块)
命名空间 IGeekFan.FreeKit.Extras.Dependency,两个 Autofac 模块:
-
FreeKitModule:扫描指定程序集,将实现以下接口的类自动注册(跳过abstract/ 泛型 / 标注[DisableConventionalRegistration]的类型):接口 生命周期 ITransientDependencyInstancePerDependency()(瞬时)IScopedDependencyInstancePerLifetimeScope()(作用域内单例)ISingletonDependencySingleInstance()(单例)若提供了拦截器类型(
interceptorServiceTypes),注册会InterceptedBy(...)。 -
UnitOfWorkModule:固定注册UnitOfWorkInterceptor/UnitOfWorkAsyncInterceptor,并对类名以Service结尾的 public 非抽象类启用接口/类拦截(EnableInterfaceInterceptors/EnableClassInterceptors+PropertiesAutowired),使[Transactional]在服务层生效。
cb.RegisterModule(new FreeKitModule([typeof(Program).Assembly]));
cb.RegisterModule(new UnitOfWorkModule([typeof(Program).Assembly]));
DisableConventionalRegistrationAttribute 用于排除某个类不参与约定式注册。
服务集合扩展(AddFreeKitCore 等)
命名空间特意放在 Microsoft.Extensions.DependencyInjection,便于 services.AddXxx() 链式调用。
// 核心聚合注册(按顺序执行)
services.AddFreeKitCore(typeUserkey = null); // HttpContextAccessor + 当前用户 + 访问器 + 审计仓储 + 复合仓储 + 默认仓储
// 工作单元管理器(使 [Transactional] 在 Controller Action 生效)
services.AddUnitOfWorkManager(); // 默认实体主键 Guid
services.AddUnitOfWorkManager<TEntity>(); // 指定默认实体主键类型
// 子能力(已被 AddFreeKitCore 内部调用,也可单独调用)
services.AddCurrentUser(); // ICurrentUser -> CurrentUser(瞬时)
services.AddCurrentUserAccessor(); // ICurrentUserAccessor -> CurrentUserAccessor(单例)
services.AddAuditRepostiory(typeof(long)); // 审计仓储(按用户主键类型映射)
services.AddCompositeRepostiory(); // 复合主键仓储
services.AddDefaultRepository(); // IBaseRepository 标准仓储
// 中间件
app.UseCurrentUserAccessor();
AddFreeKitCore(typeUserkey) 用户主键类型决定审计仓储映射:
typeUserkey | IAuditBaseRepository<> | IAuditBaseRepository<,> |
|---|---|---|
null 或 typeof(Guid)(默认) | AuditGuidRepository<> | AuditTKeyGuidRepository<,> |
typeof(long) | AuditLongRepository<> | AuditTKeyLongRepository<,> |
typeof(int) | AuditIntRepository<> | AuditTKeyIntRepository<,> |
| 其他 | 抛 NotSupportedException("用户ID仅支持Guid/long/int类型") | — |
AddUnitOfWorkManager() 注册 UnitOfWorkActionFilter(Transient)+ UnitOfWorkManager(Scoped);泛型版本注册 UnitOfWorkManager<T>。
配置项与参数速查
| 配置点 | 位置 | 关键参数 / 默认值 |
|---|---|---|
| 多租户 | MultiTenancyOptions | IsEnabled=true、EnableHeaderResolver=true(HeaderName="X-Tenant-Id")、EnableQueryStringResolver=true(QueryStringKey="tenantId")、EnableClaimResolver=true、EnableHostResolver=false、UseSubdomain=true、EnableGlobalQueryFilter=true |
| 工作单元 | UnitOfWorkDefaultOptions | Propagation=null(→Required)、IsolationLevel=null、PublishDomainEvent=true |
| 事务特性 | TransactionalAttribute | Propagation?、IsolationLevel?、IsDisabled=false |
| 当前用户 | FreeKitClaimTypes | TenantId="tenantid"、TenantName="tenantname"、NameIdentifier=用户 Id、UserName=登录名 |
| 审计仓储 | AddFreeKitCore(typeUserkey) | typeUserkey:Guid(默认)/long/int |
| 命名转换 | CaseQuery 组件 | 选择 Snake / Lower / CamelCase 三套之一 |
依赖关系与与其他包的关联
- 与
IGeekFan.FreeKit(核心):Extras 是核心契约在 Web/FreeSql 场景下的实现与装配层。核心定义接口(如IEntity、ISoftDelete、ITransientDependency、IDomainEventBase),Extras 提供具体仓储、当前用户、自动注册等。 - 与 FreeSql:几乎所有数据访问能力(仓储、
UnitOfWorkManager、审计 AOP、全局过滤)都建立在 FreeSql 之上。 - 与 Autofac:约定式自动注册与事务拦截器依赖 Autofac 的动态代理。
- 与 MediatR:事务提交时通过
IMediator发布领域事件。 - 与
IGeekFan.FreeKit.Web(上层宿主):宿主项目负责把 Extras 的各能力串起来(注册 FreeSql、AddFreeKitCore、UseMultiTenancy、Autofac 模块等)。
适用边界
| 适合使用 | 不适合 / 注意 |
|---|---|
| 基于 FreeSql 的 ASP.NET Core 应用 | 不使用 FreeSql 的项目(核心仓储/事务能力无法使用) |
| 需要自动审计、当前用户、多租户 | 仅做实体约定、无 Web 宿主 |
| 希望约定式 DI、声明式事务 | 极简应用(引入 Autofac 等额外依赖) |
| 前后端命名风格不一致 | 需自行选择 CaseQuery 风格并注册 |
故障排查
| 问题 | 解决方案 |
|---|---|
ICurrentUser 为空 | 确保 app.UseCurrentUserAccessor() 位于 UseAuthentication() 之后、UseAuthorization() 之前 |
| 审计字段未填充 | 确认实体实现了 ICreateAuditEntity<> 等接口,且通过 IAuditBaseRepository(而非 IBaseRepository)操作 |
[Transactional] 未生效(服务层) | 确认类以 Service 结尾并被 UnitOfWorkModule 扫描,且 Autofac 为 ServiceProvider |
[Transactional] 未生效(Action 层) | 确认 options.Filters.AddService(typeof(UnitOfWorkActionFilter)) 已添加 |
| 多租户未解析 | 检查 MultiTenancyOptions 开关与解析器优先级;[IgnoreTenant] 会跳过解析 |
| 租户数据未过滤 | 确认调用了 fsql.UseMultiTenancyFilter() 且 EnableGlobalQueryFilter=true |
| 自动注册失败 | 确认类实现了 IScopedDependency 等接口且未被 [DisableConventionalRegistration] 排除 |
| 用户 Id 类型报错 | AddFreeKitCore(typeUserkey) 仅支持 Guid / long / int |