跳到主要内容

IGeekFan.FreeKit.Extras

源码位置:src/FreeKit/src/IGeekFan.FreeKit.Extras 目标框架:net8.0 / net10.0(启用 NullableImplicitUsings,并生成 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 / CurrentUserICurrentUserAccessor / CurrentUserAccessorCurrentUserAccessorMiddlewareClaimsPrincipalExtensionsCurrentUserExtensionsFreeKitClaimTypesUserAccessToken
IGeekFan.FreeKit.Extras.FreeSql仓储与事务IAuditBaseRepository<>AuditBaseRepository<,>AuditDefaultRepository<,,>AuditGuidRepository<> 等、ICompositeRepository<,,>CompositeDefaultRepository<,,>FreeSqlExtensionReflexHelperTransactionalAttributeUnitOfWorkActionFilterUnitOfWorkInterceptorUnitOfWorkAsyncInterceptorUnitOfWorkDefaultOptions
FreeSql(非常规命名空间)FreeSql 辅助FreeSqlExtensionFreeSql.Aop.AuditValueEventArgsExtension
IGeekFan.FreeKit.Extras.MultiTenancy多租户MultiTenancyOptionsAddMultiTenancy / UseMultiTenancyITenantResolver(Claim/Header/QueryString/Host)、TenantInfoITenantStoreITenantAccessor / TenantAccessorTenantMiddlewareIgnoreTenantAttributeFreeSqlMultiTenancyExtensions
IGeekFan.FreeKit.Extras.CaseQuery命名风格转换SnakeApiDescriptionProvider / Lower / CamelCaseSnakeCaseQueryValueProvider 等、SnakeCaseValueProviderFactory
IGeekFan.FreeKit.Extras.Dto分页 DTOPagedResultDto<T>
IGeekFan.FreeKit.Extras.Extensions通用扩展DateTimeExtensionsStringExtensionsEnumerableExtensionsTypeExtensions
IGeekFan.FreeKit.Extras.AuditEntity实体基类Entity / Entity<T>FullAuditEntity / FullAuditEntity<T,U>CreateAuditEntity
IGeekFan.FreeKit.Extras.DependencyAutofac 模块FreeKitModuleUnitOfWorkModule
Microsoft.Extensions.DependencyInjection服务注册ServiceCollectionExtensionsAddFreeKitCoreAddUnitOfWorkManager 等)

安装与依赖

dotnet add package IGeekFan.FreeKit.Extras

包级依赖(IGeekFan.FreeKit.Extras.csproj):

依赖用途
IGeekFan.FreeKit(ProjectReference)实体/审计接口、依赖标记、领域事件抽象、模块系统
FreeSql.DbContextORM 与仓储基类(DefaultRepositoryUnitOfWorkManager 等)
Autofac.Extensions.DependencyInjection 10.0.0Autofac 容器与 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 接口与属性

ICurrentUserIGeekFan.FreeKit.Extras.Security)从 HttpContext.User 的 Claims 中解析登录人信息。泛型版本 ICurrentUser<T>T : IEquatable<T>)用于指定用户主键类型,ICurrentUserICurrentUser<string>

成员类型Claim 来源(FreeKitClaimTypes
IsAuthenticatedbool是否存在用户 Id
IdT?NameIdentifier(用户 Id)
UserNamestring?ClaimTypes.Name(登录名)
Emailstring?ClaimTypes.Email
Rolesstring[]ClaimTypes.Role
TenantIdGuid?tenantid
TenantNamestring?tenantname
FindClaim(string) / FindClaims(string) / GetAllClaims()按声明类型取 Claim
IsInRole(string)bool是否拥有某角色

FreeKitClaimTypes 常量:TenantName="tenantname"TenantId="tenantid"NickName="nickname"Name=ClaimTypes.GivenName(姓名)、PhoneNumber=ClaimTypes.MobilePhoneUserName=ClaimTypes.NameEmail=ClaimTypes.EmailRole=ClaimTypes.RoleNameIdentifier=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);
}
}

扩展方法

  • ClaimsPrincipalExtensionsFindUserId()FindUserIdToGuid()FindUserIdToLong()FindUserIdToInt()FindTenantId()FindUserName() 等,从 ClaimsPrincipalFreeKitClaimTypes 取值。
  • CurrentUserExtensionsFindUserId<T>()FindUserIdToGuid/Long/Int()FindName()FindNickName()FindPhoneNumber()
  • CurrentUserMultiTenancyExtensionsGetCurrentTenant(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:若实体实现 ITenantTenantId 为空,写入 CurrentUser.TenantId;若实现 ICreateAuditEntity<TUkey>,仅当字段为空时写入 CreateTime=DateTime.NowCreateUserId=CurrentUser.FindUserId<TUkey>()CreateUserName=CurrentUser.UserName
  • BeforeUpdate:若 IUpdateAuditEntity<TUkey>,写入 UpdateTime / UpdateUserName / UpdateUserId
  • BeforeDelete:若实体为 ISoftDelete,置 IsDeleted=true(软删除);若 IDeleteAuditEntity<TUkey>,写入 DeleteUserId / DeleteUserName / DeleteTime

具体实现(依据"表主键 / 用户主键"组合自动选择):

类型表主键用户主键注册为
AuditGuidRepository<TEntity>GuidGuidIAuditBaseRepository<TEntity>
AuditLongRepository<TEntity>GuidlongIAuditBaseRepository<TEntity>
AuditIntRepository<TEntity>Guidlong(int 按 long 承载)IAuditBaseRepository<TEntity>
AuditTKeyGuidRepository<TEntity, TKey>自定义 TKeyGuidIAuditBaseRepository<TEntity, TKey>
AuditTKeyLongRepository<TEntity, TKey>自定义 TKeylongIAuditBaseRepository<TEntity, TKey>
AuditTKeyIntRepository<TEntity, TKey>自定义 TKeylongIAuditBaseRepository<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 不使用事务等);IsolationLevelSystem.Data.IsolationLevel

两种生效路径:

  1. 服务层拦截(推荐):UnitOfWorkModule(Autofac)为所有以 Service 结尾的类启用 UnitOfWorkInterceptor / UnitOfWorkAsyncInterceptor 接口/类拦截。被 [Transactional] 标注的方法在调用时自动开启工作单元,成功 Commit、异常 Rollback
  2. MVC Action 过滤器UnitOfWorkActionFilter 检测 Controller/Action 上的 [Transactional],开启 UnitOfWorkManager.Begin(...),Action 结束后提交/回滚(需在 AddControllersFilters.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来源默认是否启用行为
HostTenantResolver40请求 Host否(EnableHostResolver=false配置了 TenantDomainSuffix 时取子域为 Code;否则 UseSubdomain=true 且 Host ≥ 3 段时取第一段为 Code
QueryStringTenantResolver30Query 参数 tenantIdGuid 解析为 Id,否则作为 Code
HeaderTenantResolver20请求头 X-Tenant-Id同上
ClaimTenantResolver10已认证用户 ClaimFreeKitClaimTypes.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):

  • XxxApiDescriptionProviderIApiDescriptionProviderOrder=1):改写 Swagger / API 描述中的参数名为对应风格。
  • XxxCaseQueryValueProviderQueryStringValueProvider):重写查询参数绑定时的 key 大小写。
  • XxxCaseValueProviderFactoryIValueProviderFactory):向 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

主要方法说明
DateTimeExtensionsToDateTimeString(removeSecond=false)ToDateString()ToTimeString()ToMillisecondString()ToChineseDateString()ToChineseDateTimeString()Description(this TimeSpan)各类日期/时间格式化与中文格式
StringExtensionsToLong()IsNullOrEmpty/IsNotNullOrEmpty/IsNullOrWhiteSpace/IsNotNullOrWhiteSpaceToPascalCase()ToCamelCase()ToKebabCase()ToSnakeCase()ToTrainCase()判空与大小写风格转换(被 CaseQuery 复用)
EnumerableExtensionsLoopIndex<T>(this IEnumerable<T>)返回 (item, index) 序列
TypeExtensionsHasImplementedRawGeneric(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>ISoftDeleteITenant

依赖注入自动注册(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) 用户主键类型决定审计仓储映射:

typeUserkeyIAuditBaseRepository<>IAuditBaseRepository<,>
nulltypeof(Guid)(默认)AuditGuidRepository<>AuditTKeyGuidRepository<,>
typeof(long)AuditLongRepository<>AuditTKeyLongRepository<,>
typeof(int)AuditIntRepository<>AuditTKeyIntRepository<,>
其他NotSupportedException("用户ID仅支持Guid/long/int类型")

AddUnitOfWorkManager() 注册 UnitOfWorkActionFilter(Transient)+ UnitOfWorkManager(Scoped);泛型版本注册 UnitOfWorkManager<T>

配置项与参数速查

配置点位置关键参数 / 默认值
多租户MultiTenancyOptionsIsEnabled=trueEnableHeaderResolver=true(HeaderName="X-Tenant-Id")、EnableQueryStringResolver=true(QueryStringKey="tenantId")、EnableClaimResolver=trueEnableHostResolver=falseUseSubdomain=trueEnableGlobalQueryFilter=true
工作单元UnitOfWorkDefaultOptionsPropagation=null(→Required)、IsolationLevel=nullPublishDomainEvent=true
事务特性TransactionalAttributePropagation?IsolationLevel?IsDisabled=false
当前用户FreeKitClaimTypesTenantId="tenantid"TenantName="tenantname"NameIdentifier=用户 Id、UserName=登录名
审计仓储AddFreeKitCore(typeUserkey)typeUserkeyGuid(默认)/long/int
命名转换CaseQuery 组件选择 Snake / Lower / CamelCase 三套之一

依赖关系与与其他包的关联

  • IGeekFan.FreeKit(核心):Extras 是核心契约在 Web/FreeSql 场景下的实现与装配层。核心定义接口(如 IEntityISoftDeleteITransientDependencyIDomainEventBase),Extras 提供具体仓储、当前用户、自动注册等。
  • 与 FreeSql:几乎所有数据访问能力(仓储、UnitOfWorkManager、审计 AOP、全局过滤)都建立在 FreeSql 之上。
  • 与 Autofac:约定式自动注册与事务拦截器依赖 Autofac 的动态代理。
  • 与 MediatR:事务提交时通过 IMediator 发布领域事件。
  • IGeekFan.FreeKit.Web(上层宿主):宿主项目负责把 Extras 的各能力串起来(注册 FreeSql、AddFreeKitCoreUseMultiTenancy、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

相关文档