* refactor: ♻️ 业务代码项目文件名与框架代码项目文件名区分

* refactor: ♻️ 业务代码项目文件名与框架代码项目文件名区分

---------

Co-authored-by: tk <fiyne1a@dingtalk.com>
This commit is contained in:
nsnail 2024-11-12 18:44:13 +08:00 committed by GitHub
parent e6ce5afd99
commit 27aafacd54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
520 changed files with 27576 additions and 0 deletions

View File

@ -0,0 +1,31 @@
namespace NetAdmin.Application.Extensions;
/// <summary>
/// 工作单元管理器扩展方法
/// </summary>
public static class UnitOfWorkManagerExtensions
{
/// <summary>
/// 事务操作
/// </summary>
public static async Task AtomicOperateAsync(this UnitOfWorkManager me, Func<Task> handle)
{
var logger = LogHelper.Get<UnitOfWorkManager>();
using var unitOfWork = me.Begin();
var hashCode = unitOfWork.GetHashCode();
try {
#if DEBUG
logger?.Debug($"{Ln.开始事务}: {hashCode}");
#endif
await handle().ConfigureAwait(false);
unitOfWork.Commit();
logger?.Info($"{Ln.事务已提交}: {hashCode}");
}
catch (Exception ex) {
logger?.Warn(ex);
unitOfWork.Rollback();
logger?.Warn($"{Ln.事务已回滚}: {hashCode}");
throw;
}
}
}

View File

@ -0,0 +1,65 @@
using NetAdmin.Domain;
using NetAdmin.Domain.Dto.Dependency;
namespace NetAdmin.Application.Modules;
/// <summary>
/// 增删改查模块接口
/// </summary>
/// <typeparam name="TCreateReq">创建请求类型</typeparam>
/// <typeparam name="TCreateRsp">创建响应类型</typeparam>
/// <typeparam name="TQueryReq">查询请求类型</typeparam>
/// <typeparam name="TQueryRsp">查询响应类型</typeparam>
/// <typeparam name="TDelReq">删除请求类型</typeparam>
public interface ICrudModule<in TCreateReq, TCreateRsp, TQueryReq, TQueryRsp, TDelReq>
where TCreateReq : DataAbstraction, new()
where TCreateRsp : DataAbstraction
where TQueryReq : DataAbstraction, new()
where TQueryRsp : DataAbstraction
where TDelReq : DataAbstraction, new()
{
/// <summary>
/// 批量删除实体
/// </summary>
Task<int> BulkDeleteAsync(BulkReq<TDelReq> req);
/// <summary>
/// 实体计数
/// </summary>
Task<long> CountAsync(QueryReq<TQueryReq> req);
/// <summary>
/// 创建实体
/// </summary>
Task<TCreateRsp> CreateAsync(TCreateReq req);
/// <summary>
/// 删除实体
/// </summary>
Task<int> DeleteAsync(TDelReq req);
/// <summary>
/// 判断实体是否存在
/// </summary>
Task<bool> ExistAsync(QueryReq<TQueryReq> req);
/// <summary>
/// 导出实体
/// </summary>
Task<IActionResult> ExportAsync(QueryReq<TQueryReq> req);
/// <summary>
/// 获取单个实体
/// </summary>
Task<TQueryRsp> GetAsync(TQueryReq req);
/// <summary>
/// 分页查询实体
/// </summary>
Task<PagedQueryRsp<TQueryRsp>> PagedQueryAsync(PagedQueryReq<TQueryReq> req);
/// <summary>
/// 查询实体
/// </summary>
Task<IEnumerable<TQueryRsp>> QueryAsync(QueryReq<TQueryReq> req);
}

View File

@ -0,0 +1,12 @@
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Tpl.Example;
namespace NetAdmin.Application.Modules.Tpl;
/// <summary>
/// 示例模块
/// </summary>
public interface IExampleModule : ICrudModule<CreateExampleReq, QueryExampleRsp // 创建类型
, QueryExampleReq, QueryExampleRsp // 查询类型
, DelReq // 删除类型
>;

View File

@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)/build/code.quality.props"/>
<ItemGroup>
<ProjectReference Include="../NetAdmin.Domain/NetAdmin.Domain.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,18 @@
using NetAdmin.Domain.Contexts;
using NetAdmin.Domain.DbMaps.Dependency;
namespace NetAdmin.Application.Repositories;
/// <summary>
/// 基础仓储
/// </summary>
public sealed class BasicRepository<TEntity, TPrimary>(IFreeSql fSql, UnitOfWorkManager uowManger, ContextUserToken userToken)
: DefaultRepository<TEntity, TPrimary>(fSql, uowManger)
where TEntity : EntityBase<TPrimary> //
where TPrimary : IEquatable<TPrimary>
{
/// <summary>
/// 当前上下文关联的用户令牌
/// </summary>
public ContextUserToken UserToken => userToken;
}

View File

@ -0,0 +1,19 @@
using NetAdmin.Domain.Contexts;
namespace NetAdmin.Application.Services;
/// <summary>
/// 服务接口
/// </summary>
public interface IService
{
/// <summary>
/// 服务编号
/// </summary>
Guid ServiceId { get; init; }
/// <summary>
/// 上下文用户令牌
/// </summary>
ContextUserToken UserToken { get; set; }
}

View File

@ -0,0 +1,43 @@
using NetAdmin.Application.Repositories;
using NetAdmin.Domain.DbMaps.Dependency;
using StackExchange.Redis;
namespace NetAdmin.Application.Services;
/// <summary>
/// Redis Service Base
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="RedisService{TEntity, TPrimary, TLogger}" /> class.
/// Redis Service Base
/// </remarks>
public abstract class RedisService<TEntity, TPrimary, TLogger>(BasicRepository<TEntity, TPrimary> rpo)
: RepositoryService<TEntity, TPrimary, TLogger>(rpo)
where TEntity : EntityBase<TPrimary> //
where TPrimary : IEquatable<TPrimary>
{
/// <summary>
/// Redis Database
/// </summary>
protected IDatabase RedisDatabase { get; } //
= App.GetService<IConnectionMultiplexer>()
.GetDatabase(App.GetOptions<RedisOptions>().Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE).Database);
/// <summary>
/// 获取锁
/// </summary>
protected Task<RedisLocker> GetLockerAsync(string lockerName)
{
return RedisLocker.GetLockerAsync(RedisDatabase, lockerName, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY)
, Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY));
}
/// <summary>
/// 获取锁(仅获取一次)
/// </summary>
protected Task<RedisLocker> GetLockerOnceAsync(string lockerName)
{
return RedisLocker.GetLockerAsync(RedisDatabase, lockerName, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), 1
, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY));
}
}

View File

@ -0,0 +1,159 @@
using CsvHelper;
using Microsoft.Net.Http.Headers;
using NetAdmin.Application.Repositories;
using NetAdmin.Domain;
using NetAdmin.Domain.DbMaps.Dependency;
using NetAdmin.Domain.DbMaps.Dependency.Fields;
using NetAdmin.Domain.Dto.Dependency;
namespace NetAdmin.Application.Services;
/// <summary>
/// 仓储服务基类
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TPrimary">主键类型</typeparam>
/// <typeparam name="TLogger">日志类型</typeparam>
public abstract class RepositoryService<TEntity, TPrimary, TLogger>(BasicRepository<TEntity, TPrimary> rpo) : ServiceBase<TLogger>
where TEntity : EntityBase<TPrimary> //
where TPrimary : IEquatable<TPrimary>
{
/// <summary>
/// 默认仓储
/// </summary>
protected BasicRepository<TEntity, TPrimary> Rpo => rpo;
/// <summary>
/// 启用级联保存
/// </summary>
protected bool EnableCascadeSave {
get => Rpo.DbContextOptions.EnableCascadeSave;
set => Rpo.DbContextOptions.EnableCascadeSave = value;
}
/// <summary>
/// 导出实体
/// </summary>
protected static async Task<IActionResult> ExportAsync<TQuery, TExport>( //
Func<QueryReq<TQuery>, ISelectGrouping<TEntity, TEntity>> selector, QueryReq<TQuery> query, string fileName
, Expression<Func<ISelectGroupingAggregate<TEntity, TEntity>, object>> listExp = null)
where TQuery : DataAbstraction, new()
{
var list = await selector(query).Take(Numbers.MAX_LIMIT_EXPORT).ToListAsync(listExp).ConfigureAwait(false);
return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false);
}
/// <summary>
/// 导出实体
/// </summary>
protected static async Task<IActionResult> ExportAsync<TQuery, TExport>( //
Func<QueryReq<TQuery>, ISelect<TEntity>> selector, QueryReq<TQuery> query, string fileName, Expression<Func<TEntity, object>> listExp = null)
where TQuery : DataAbstraction, new()
{
var select = selector(query)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Take(Numbers.MAX_LIMIT_EXPORT);
object list = listExp == null ? await select.ToListAsync().ConfigureAwait(false) : await select.ToListAsync(listExp).ConfigureAwait(false);
return await GetExportFileStreamAsync<TExport>(fileName, list).ConfigureAwait(false);
}
/// <summary>
/// 更新实体
/// </summary>
/// <param name="newValue">新的值</param>
/// <param name="includeFields">包含的属性</param>
/// <param name="excludeFields">排除的属性</param>
/// <param name="whereExp">查询表达式</param>
/// <param name="whereSql">查询sql</param>
/// <param name="ignoreVersion">是否忽略版本锁</param>
/// <returns>更新行数</returns>
protected Task<int> UpdateAsync( //
TEntity newValue //
, IEnumerable<string> includeFields //
, string[] excludeFields = null //
, Expression<Func<TEntity, bool>> whereExp = null //
, string whereSql = null //
, bool ignoreVersion = false)
{
// 默认匹配主键
whereExp ??= a => a.Id.Equals(newValue.Id);
var update = BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql);
return update.ExecuteAffrowsAsync();
}
#if DBTYPE_SQLSERVER
/// <summary>
/// 更新实体
/// </summary>
/// <param name="newValue">新的值</param>
/// <param name="includeFields">包含的属性</param>
/// <param name="excludeFields">排除的属性</param>
/// <param name="whereExp">查询表达式</param>
/// <param name="whereSql">查询sql</param>
/// <param name="ignoreVersion">是否忽略版本锁</param>
/// <returns>更新后的实体列表</returns>
protected Task<List<TEntity>> UpdateReturnListAsync( //
TEntity newValue //
, IEnumerable<string> includeFields //
, string[] excludeFields = null //
, Expression<Func<TEntity, bool>> whereExp = null //
, string whereSql = null //
, bool ignoreVersion = false)
{
// 默认匹配主键
whereExp ??= a => a.Id.Equals(newValue.Id);
return BuildUpdate(newValue, includeFields, excludeFields, ignoreVersion).Where(whereExp).Where(whereSql).ExecuteUpdatedAsync();
}
#endif
private static async Task<IActionResult> GetExportFileStreamAsync<TExport>(string fileName, object list)
{
var listTyped = list.Adapt<List<TExport>>();
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteHeader<TExport>();
await csv.NextRecordAsync().ConfigureAwait(false);
foreach (var item in listTyped) {
csv.WriteRecord(item);
await csv.NextRecordAsync().ConfigureAwait(false);
}
await csv.FlushAsync().ConfigureAwait(false);
_ = stream.Seek(0, SeekOrigin.Begin);
App.HttpContext.Response.Headers.ContentDisposition
= new ContentDispositionHeaderValue(Chars.FLG_HTTP_HEADER_VALUE_ATTACHMENT) {
FileNameStar
= $"{fileName}_{DateTime.Now:yyyy.MM.dd-HH.mm.ss}.csv"
}.ToString();
return new FileStreamResult(stream, Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_OCTET_STREAM);
}
private IUpdate<TEntity> BuildUpdate( //
TEntity entity //
, IEnumerable<string> includeFields //
, string[] excludeFields = null //
, bool ignoreVersion = false)
{
var updateExp = includeFields == null
? Rpo.UpdateDiy.SetSource(entity)
: Rpo.UpdateDiy.SetDto(includeFields!.ToDictionary(
x => x, x => typeof(TEntity).GetProperty(x, BindingFlags.Public | BindingFlags.Instance)!.GetValue(entity)));
if (excludeFields != null) {
updateExp = updateExp.IgnoreColumns(excludeFields);
}
if (!ignoreVersion && entity is IFieldVersion ver) {
updateExp = updateExp.Where($"{nameof(IFieldVersion.Version)} = @version", new { version = ver.Version });
}
return updateExp;
}
}

View File

@ -0,0 +1,41 @@
using NetAdmin.Domain.Contexts;
namespace NetAdmin.Application.Services;
/// <inheritdoc />
public abstract class ServiceBase<TLogger> : ServiceBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceBase{TLogger}" /> class.
/// </summary>
protected ServiceBase() //
{
Logger = App.GetService<ILogger<TLogger>>();
}
/// <summary>
/// 日志记录器
/// </summary>
protected ILogger<TLogger> Logger { get; }
}
/// <summary>
/// 服务基类
/// </summary>
public abstract class ServiceBase : IScoped, IService
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceBase" /> class.
/// </summary>
protected ServiceBase()
{
UserToken = App.GetService<ContextUserToken>();
ServiceId = Guid.NewGuid();
}
/// <inheritdoc />
public Guid ServiceId { get; init; }
/// <inheritdoc />
public ContextUserToken UserToken { get; set; }
}

View File

@ -0,0 +1,8 @@
using NetAdmin.Application.Modules.Tpl;
namespace NetAdmin.Application.Services.Tpl.Dependency;
/// <summary>
/// 示例服务
/// </summary>
public interface IExampleService : IService, IExampleModule;

View File

@ -0,0 +1,128 @@
using NetAdmin.Application.Repositories;
using NetAdmin.Application.Services.Tpl.Dependency;
using NetAdmin.Domain.DbMaps.Tpl;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Tpl.Example;
namespace NetAdmin.Application.Services.Tpl;
/// <inheritdoc cref="IExampleService" />
public sealed class ExampleService(BasicRepository<Tpl_Example, long> rpo) //
: RepositoryService<Tpl_Example, long, IExampleService>(rpo), IExampleService
{
/// <inheritdoc />
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
req.ThrowIfInvalid();
var ret = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in req.Items) {
ret += await DeleteAsync(item).ConfigureAwait(false);
}
return ret;
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryExampleReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.CountAsync();
}
/// <inheritdoc />
public async Task<QueryExampleRsp> CreateAsync(CreateExampleReq req)
{
req.ThrowIfInvalid();
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
return ret.Adapt<QueryExampleRsp>();
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
req.ThrowIfInvalid();
return Rpo.DeleteAsync(a => a.Id == req.Id);
}
/// <inheritdoc />
public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.AnyAsync();
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req)
{
req.ThrowIfInvalid();
return ExportAsync<QueryExampleReq, QueryExampleRsp>(QueryInternal, req, Ln.);
}
/// <inheritdoc />
public async Task<QueryExampleRsp> GetAsync(QueryExampleReq req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(new QueryReq<QueryExampleReq> { Filter = req, Order = Orders.None }).ToOneAsync().ConfigureAwait(false);
return ret.Adapt<QueryExampleRsp>();
}
/// <inheritdoc />
public async Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req)
{
req.ThrowIfInvalid();
var list = await QueryInternal(req)
.Page(req.Page, req.PageSize)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Count(out var total)
.ToListAsync()
.ConfigureAwait(false);
return new PagedQueryRsp<QueryExampleRsp>(req.Page, req.PageSize, total, list.Adapt<IEnumerable<QueryExampleRsp>>());
}
/// <inheritdoc />
public async Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Take(req.Count)
.ToListAsync()
.ConfigureAwait(false);
return ret.Adapt<IEnumerable<QueryExampleRsp>>();
}
private ISelect<Tpl_Example> QueryInternal(QueryReq<QueryExampleReq> req)
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter);
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (req.Order) {
case Orders.None:
return ret;
case Orders.Random:
return ret.OrderByRandom();
}
ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id);
}
return ret;
}
}

View File

@ -0,0 +1,16 @@
using NetAdmin.Application.Services;
namespace NetAdmin.Cache;
/// <summary>
/// 缓存基类
/// </summary>
public abstract class CacheBase<TCacheContainer, TService>(TCacheContainer cache, TService service) : ICache<TCacheContainer, TService>
where TService : IService
{
/// <inheritdoc />
public TCacheContainer Cache => cache;
/// <inheritdoc />
public TService Service => service;
}

View File

@ -0,0 +1,93 @@
using System.Runtime.CompilerServices;
using NetAdmin.Application.Services;
namespace NetAdmin.Cache;
/// <summary>
/// 分布式缓存
/// </summary>
public abstract class DistributedCache<TService>(IDistributedCache cache, TService service) : CacheBase<IDistributedCache, TService>(cache, service)
where TService : IService
{
/// <summary>
/// 创建缓存
/// </summary>
/// <param name="key">缓存键</param>
/// <param name="createObj">创建对象</param>
/// <param name="absLifeTime">绝对过期时间</param>
/// <param name="slideLifeTime">滑动过期时间</param>
/// <typeparam name="T">缓存对象类型</typeparam>
/// <returns>缓存对象</returns>
protected Task CreateAsync<T>(string key, T createObj, TimeSpan? absLifeTime = null, TimeSpan? slideLifeTime = null)
{
var cacheWrite = createObj.ToJson();
var options = new DistributedCacheEntryOptions();
if (absLifeTime != null) {
_ = options.SetAbsoluteExpiration(absLifeTime.Value);
}
if (slideLifeTime != null) {
_ = options.SetSlidingExpiration(slideLifeTime.Value);
}
return Cache.SetAsync(key, cacheWrite.Hex(), options);
}
/// <summary>
/// 获取缓存
/// </summary>
protected async Task<T> GetAsync<T>(string key)
{
var cacheRead = await Cache.GetStringAsync(key).ConfigureAwait(false);
try {
return cacheRead != null ? cacheRead.ToObject<T>() : default;
}
catch (JsonException) {
return default;
}
}
/// <summary>
/// 获取缓存键
/// </summary>
protected string GetCacheKey(string id = "0", [CallerMemberName] string memberName = null)
{
return $"{GetType().FullName}.{memberName}.{id}";
}
/// <summary>
/// 获取或创建缓存
/// </summary>
/// <param name="key">缓存键</param>
/// <param name="createProc">创建函数</param>
/// <param name="absLifeTime">绝对过期时间</param>
/// <param name="slideLifeTime">滑动过期时间</param>
/// <typeparam name="T">缓存对象类型</typeparam>
/// <returns>缓存对象</returns>
protected async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> createProc, TimeSpan? absLifeTime = null, TimeSpan? slideLifeTime = null)
{
var cacheRead = await GetAsync<T>(key).ConfigureAwait(false);
if (cacheRead is not null && App.HttpContext?.Request.Headers.CacheControl.FirstOrDefault() != Chars.FLG_HTTP_HEADER_VALUE_NO_CACHE) {
return cacheRead;
}
var obj = await createProc.Invoke().ConfigureAwait(false);
var cacheWrite = obj?.ToJson();
if (cacheWrite == null) {
return obj;
}
await CreateAsync(key, obj, absLifeTime, slideLifeTime).ConfigureAwait(false);
return obj;
}
/// <summary>
/// 删除缓存
/// </summary>
protected Task RemoveAsync(string key)
{
return Cache.RemoveAsync(key);
}
}

View File

@ -0,0 +1,20 @@
using NetAdmin.Application.Services;
namespace NetAdmin.Cache;
/// <summary>
/// 缓存接口
/// </summary>
public interface ICache<out TCacheLoad, out TService>
where TService : IService
{
/// <summary>
/// 缓存对象
/// </summary>
TCacheLoad Cache { get; }
/// <summary>
/// 关联的服务
/// </summary>
public TService Service { get; }
}

View File

@ -0,0 +1,9 @@
using NetAdmin.Application.Services;
namespace NetAdmin.Cache;
/// <summary>
/// 内存缓存
/// </summary>
public abstract class MemoryCache<TService>(IMemoryCache cache, TService service) : CacheBase<IMemoryCache, TService>(cache, service)
where TService : IService;

View File

@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)/build/code.quality.props"/>
<ItemGroup>
<ProjectReference Include="../NetAdmin.Application/NetAdmin.Application.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
using NetAdmin.Application.Modules.Tpl;
using NetAdmin.Application.Services.Tpl.Dependency;
namespace NetAdmin.Cache.Tpl.Dependency;
/// <summary>
/// 示例缓存
/// </summary>
public interface IExampleCache : ICache<IDistributedCache, IExampleService>, IExampleModule;

View File

@ -0,0 +1,65 @@
using NetAdmin.Application.Services.Tpl.Dependency;
using NetAdmin.Cache.Tpl.Dependency;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Tpl.Example;
namespace NetAdmin.Cache.Tpl;
/// <inheritdoc cref="IExampleCache" />
public sealed class ExampleCache(IDistributedCache cache, IExampleService service)
: DistributedCache<IExampleService>(cache, service), IScoped, IExampleCache
{
/// <inheritdoc />
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Service.BulkDeleteAsync(req);
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryExampleReq> req)
{
return Service.CountAsync(req);
}
/// <inheritdoc />
public Task<QueryExampleRsp> CreateAsync(CreateExampleReq req)
{
return Service.CreateAsync(req);
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
return Service.DeleteAsync(req);
}
/// <inheritdoc />
public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req)
{
return Service.ExistAsync(req);
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req)
{
return Service.ExportAsync(req);
}
/// <inheritdoc />
public Task<QueryExampleRsp> GetAsync(QueryExampleReq req)
{
return Service.GetAsync(req);
}
/// <inheritdoc />
public Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req)
{
return Service.PagedQueryAsync(req);
}
/// <inheritdoc />
public Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req)
{
return Service.QueryAsync(req);
}
}

View File

@ -0,0 +1,7 @@
namespace NetAdmin.Domain.Attributes;
/// <summary>
/// 危险字段标记
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class DangerFieldAttribute : Attribute;

View File

@ -0,0 +1,23 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 支付宝验证器(手机或邮箱)
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class AlipayAttribute : ValidationAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AlipayAttribute" /> class.
/// </summary>
public AlipayAttribute()
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
/// <inheritdoc />
public override bool IsValid(object value)
{
return new MobileAttribute().IsValid(value) || new EmailAddressAttribute().IsValid(value);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 证件号码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class CertificateAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CertificateAttribute" /> class.
/// </summary>
public CertificateAttribute() //
: base(Chars.RGX_CERTIFICATE)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 中文姓名验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class ChineseNameAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ChineseNameAttribute" /> class.
/// </summary>
public ChineseNameAttribute() //
: base(Chars.RGXL_CHINESE_NAME)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 时间表达式验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class CronAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CronAttribute" /> class.
/// </summary>
public CronAttribute() //
: base(Chars.RGXL_CRON)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 邮箱验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class EmailAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="EmailAttribute" /> class.
/// </summary>
public EmailAttribute() //
: base(Chars.RGXL_EMAIL)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 邀请码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class InviteCodeAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="InviteCodeAttribute" /> class.
/// </summary>
public InviteCodeAttribute() //
: base(Chars.RGX_INVITE_CODE)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,14 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// JSON文本验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class JsonStringAttribute : ValidationAttribute
{
/// <inheritdoc />
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return (value as string).IsJsonString() ? ValidationResult.Success : new ValidationResult(Ln.JSON字符串, [validationContext.MemberName]);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 手机号码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class MobileAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="MobileAttribute" /> class.
/// </summary>
public MobileAttribute() //
: base(Chars.RGX_MOBILE)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 密码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class PasswordAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PasswordAttribute" /> class.
/// </summary>
public PasswordAttribute() //
: base(Chars.RGX_PASSWORD)
{
ErrorMessageResourceName = nameof(Ln._8位以上数字字母组合);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 交易密码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class PayPasswordAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PayPasswordAttribute" /> class.
/// </summary>
public PayPasswordAttribute() //
: base(Chars.RGX_PAY_PASSWORD)
{
ErrorMessageResourceName = nameof(Ln._6位数字);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 端口号验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class PortAttribute : RangeAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PortAttribute" /> class.
/// </summary>
public PortAttribute() //
: base(1, ushort.MaxValue)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,16 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 正则表达式验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
#pragma warning disable DesignedForInheritance
public class RegexAttribute : RegularExpressionAttribute
#pragma warning restore DesignedForInheritance
{
/// <summary>
/// Initializes a new instance of the <see cref="RegexAttribute" /> class.
/// </summary>
protected RegexAttribute(string pattern) //
: base(pattern) { }
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 固定电话验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class TelephoneAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="TelephoneAttribute" /> class.
/// </summary>
public TelephoneAttribute() //
: base(Chars.RGX_TELEPHONE)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,34 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 用户名验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class UserNameAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="UserNameAttribute" /> class.
/// </summary>
public UserNameAttribute() //
: base(Chars.RGX_USERNAME)
{
ErrorMessageResourceType = typeof(Ln);
}
/// <inheritdoc />
public override bool IsValid(object value)
{
if (!base.IsValid(value)) {
ErrorMessageResourceName = nameof(Ln.4);
return false;
}
if (!new MobileAttribute().IsValid(value)) {
return true;
}
// 不能是手机号码
ErrorMessageResourceName = nameof(Ln.);
return false;
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Domain.Attributes.DataValidation;
/// <summary>
/// 验证码验证器
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)]
public sealed class VerifyCodeAttribute : RegexAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="VerifyCodeAttribute" /> class.
/// </summary>
public VerifyCodeAttribute() //
: base(Chars.RGX_VERIFY_CODE)
{
ErrorMessageResourceName = nameof(Ln.);
ErrorMessageResourceType = typeof(Ln);
}
}

View File

@ -0,0 +1,14 @@
namespace NetAdmin.Domain.Attributes;
/// <summary>
/// 标记一个枚举的状态指示
/// </summary>
/// <inheritdoc />
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Enum)]
public sealed class IndicatorAttribute(string indicate) : Attribute
{
/// <summary>
/// 状态指示
/// </summary>
public string Indicate { get; } = indicate;
}

View File

@ -0,0 +1,7 @@
namespace NetAdmin.Domain.Attributes;
/// <summary>
/// 标记一个字段启用服务器时间
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ServerTimeAttribute : Attribute;

View File

@ -0,0 +1,9 @@
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
namespace NetAdmin.Domain.Attributes;
/// <summary>
/// 标记一个字段启用雪花编号生成
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class SnowflakeAttribute : Attribute;

View File

@ -0,0 +1,58 @@
namespace NetAdmin.Domain.Contexts;
/// <summary>
/// 上下文用户凭据
/// </summary>
public sealed record ContextUserToken : DataAbstraction
{
/// <summary>
/// 部门编号
/// </summary>
/// ReSharper disable once MemberCanBePrivate.Global
public long DeptId { get; init; }
/// <summary>
/// 用户编号
/// </summary>
/// ReSharper disable once MemberCanBePrivate.Global
public long Id { get; init; }
/// <summary>
/// 做授权验证的Token全局唯一可以随时重置强制下线
/// </summary>
/// ReSharper disable once MemberCanBePrivate.Global
public Guid Token { get; init; }
/// <summary>
/// 用户名
/// </summary>
/// ReSharper disable once MemberCanBePrivate.Global
public string UserName { get; init; }
/// <summary>
/// 从HttpContext 创建上下文用户
/// </summary>
public static ContextUserToken Create()
{
var claim = App.User?.FindFirst(nameof(ContextUserToken));
return claim?.Value.ToObject<ContextUserToken>();
}
/// <summary>
/// 从 QueryUserRsp 创建上下文用户
/// </summary>
public static ContextUserToken Create(long id, Guid token, string userName, long deptId)
{
return new ContextUserToken { Id = id, Token = token, UserName = userName, DeptId = deptId };
}
/// <summary>
/// 从 Json Web Token 创建上下文用户
/// </summary>
public static ContextUserToken Create(string jwt)
{
var claim = JWTEncryption.ReadJwtToken(jwt.TrimPrefix($"{Chars.FLG_HTTP_HEADER_VALUE_AUTH_SCHEMA} "))
?.Claims.FirstOrDefault(x => x.Type == nameof(ContextUserToken));
return claim?.Value.ToObject<ContextUserToken>();
}
}

View File

@ -0,0 +1,48 @@
namespace NetAdmin.Domain;
/// <summary>
/// 数据基类
/// </summary>
public abstract record DataAbstraction
{
/// <summary>
/// 如果数据校验失败,抛出异常
/// </summary>
/// <exception cref="NetAdminValidateException">NetAdminValidateException</exception>
public void ThrowIfInvalid()
{
var validationResult = this.TryValidate();
if (!validationResult.IsValid) {
throw new NetAdminValidateException(validationResult.ValidationResults.ToDictionary( //
x => x.MemberNames.First() //
, x => new[] { x.ErrorMessage }));
}
}
/// <inheritdoc />
public override string ToString()
{
return this.ToJson();
}
/// <summary>
/// 截断所有字符串属性 以符合[MaxLength(x)]特性
/// </summary>
public void TruncateStrings()
{
foreach (var property in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.PropertyType == typeof(string))) {
var maxLen = property.GetCustomAttribute<MaxLengthAttribute>(true)?.Length;
if (maxLen is null or 0) {
continue;
}
var value = property.GetValue(this);
if (value is not string s || s.Length < maxLen) {
continue;
}
s = s.Sub(0, maxLen.Value);
property.SetValue(this, s);
}
}
}

View File

@ -0,0 +1,13 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <summary>
/// 数据库实体基类
/// </summary>
public abstract record EntityBase<T> : DataAbstraction
where T : IEquatable<T>
{
/// <summary>
/// 唯一编码
/// </summary>
public virtual T Id { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 创建者客户端IP字段接口
/// </summary>
public interface IFieldCreatedClientIp
{
/// <summary>
/// 创建者客户端IP
/// </summary>
int? CreatedClientIp { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 创建者客户端用户代理字段接口
/// </summary>
public interface IFieldCreatedClientUserAgent
{
/// <summary>
/// 创建者客户端用户代理
/// </summary>
string CreatedUserAgent { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 创建时间字段接口
/// </summary>
public interface IFieldCreatedTime
{
/// <summary>
/// 创建时间
/// </summary>
DateTime CreatedTime { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 创建用户字段接口
/// </summary>
public interface IFieldCreatedUser
{
/// <summary>
/// 创建者编号
/// </summary>
long? CreatedUserId { get; init; }
/// <summary>
/// 创建者用户名
/// </summary>
string CreatedUserName { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 启用字段接口
/// </summary>
public interface IFieldEnabled
{
/// <summary>
/// 是否启用
/// </summary>
bool Enabled { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 修改客户端IP字段接口
/// </summary>
public interface IFieldModifiedClientIp
{
/// <summary>
/// 客户端IP
/// </summary>
int ModifiedClientIp { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 修改客户端用户代理字段接口
/// </summary>
public interface IFieldModifiedClientUserAgent
{
/// <summary>
/// 客户端用户代理
/// </summary>
string ModifiedUserAgent { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 修改时间字段接口
/// </summary>
public interface IFieldModifiedTime
{
/// <summary>
/// 修改时间
/// </summary>
DateTime? ModifiedTime { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 修改用户字段接口
/// </summary>
public interface IFieldModifiedUser
{
/// <summary>
/// 修改者编号
/// </summary>
long? ModifiedUserId { get; init; }
/// <summary>
/// 修改者用户名
/// </summary>
string ModifiedUserName { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 拥有者字段接口
/// </summary>
public interface IFieldOwner
{
/// <summary>
/// 拥有者部门编号
/// </summary>
long? OwnerDeptId { get; init; }
/// <summary>
/// 拥有者用户编号
/// </summary>
long? OwnerId { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 排序字段接口
/// </summary>
public interface IFieldSort
{
/// <summary>
/// 排序值,越大越前
/// </summary>
long Sort { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 备注字段接口
/// </summary>
public interface IFieldSummary
{
/// <summary>
/// 备注
/// </summary>
string Summary { get; init; }
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.DbMaps.Dependency.Fields;
/// <summary>
/// 版本字段接口
/// </summary>
public interface IFieldVersion
{
/// <summary>
/// 数据版本
/// </summary>
long Version { get; init; }
}

View File

@ -0,0 +1,44 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record ImmutableEntity : ImmutableEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 不可变实体
/// </summary>
/// <typeparam name="T">主键类型</typeparam>
public abstract record ImmutableEntity<T> : LiteImmutableEntity<T>, IFieldCreatedUser
where T : IEquatable<T>
{
/// <summary>
/// 创建者编号
/// </summary>
[Column(CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public long? CreatedUserId { get; init; }
/// <summary>
/// 创建者用户名
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual string CreatedUserName { get; init; }
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
public override T Id { get; init; }
}

View File

@ -0,0 +1,37 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record LiteImmutableEntity : LiteImmutableEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 轻型不可变实体
/// </summary>
/// <typeparam name="T">主键类型</typeparam>
public abstract record LiteImmutableEntity<T> : EntityBase<T>, IFieldCreatedTime
where T : IEquatable<T>
{
/// <summary>
/// 创建时间
/// </summary>
[Column(ServerTime = DateTimeKind.Local, CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual DateTime CreatedTime { get; init; }
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[JsonIgnore]
public override T Id { get; init; }
}

View File

@ -0,0 +1,35 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record LiteMutableEntity : LiteMutableEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 轻型可变实体
/// </summary>
public abstract record LiteMutableEntity<T> : LiteImmutableEntity<T>, IFieldModifiedTime
where T : IEquatable<T>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
public override T Id { get; init; }
/// <summary>
/// 修改时间
/// </summary>
[Column(ServerTime = DateTimeKind.Local, CanInsert = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual DateTime? ModifiedTime { get; init; }
}

View File

@ -0,0 +1,36 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record LiteVersionEntity : LiteVersionEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 乐观锁轻型可变实体
/// </summary>
public abstract record LiteVersionEntity<T> : LiteMutableEntity<T>, IFieldVersion
where T : IEquatable<T>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override T Id { get; init; }
/// <summary>
/// 数据版本
/// </summary>
[Column(IsVersion = true, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual long Version { get; init; }
}

View File

@ -0,0 +1,59 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record MutableEntity : MutableEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 可变实体
/// </summary>
public abstract record MutableEntity<T> : LiteMutableEntity<T>, IFieldCreatedUser, IFieldModifiedUser
where T : IEquatable<T>
{
/// <summary>
/// 创建者编号
/// </summary>
[Column(CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual long? CreatedUserId { get; init; }
/// <summary>
/// 创建者用户名
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual string CreatedUserName { get; init; }
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
public override T Id { get; init; }
/// <summary>
/// 修改者编号
/// </summary>
[Column(CanInsert = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public long? ModifiedUserId { get; init; }
/// <summary>
/// 修改者用户名
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public string ModifiedUserName { get; init; }
}

View File

@ -0,0 +1,19 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record SimpleEntity : SimpleEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 简单实体
/// </summary>
public abstract record SimpleEntity<T> : EntityBase<T>
where T : IEquatable<T>;

View File

@ -0,0 +1,59 @@
namespace NetAdmin.Domain.DbMaps.Dependency;
/// <inheritdoc />
public abstract record VersionEntity : VersionEntity<long>
{
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
[Snowflake]
public override long Id { get; init; }
}
/// <summary>
/// 乐观锁可变实体
/// </summary>
public abstract record VersionEntity<T> : LiteVersionEntity<T>, IFieldModifiedUser, IFieldCreatedUser
where T : IEquatable<T>
{
/// <summary>
/// 创建者编号
/// </summary>
[Column(CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual long? CreatedUserId { get; init; }
/// <summary>
/// 创建者用户名
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanUpdate = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual string CreatedUserName { get; init; }
/// <summary>
/// 唯一编码
/// </summary>
[Column(IsIdentity = false, IsPrimary = true, Position = 1)]
[CsvIgnore]
public override T Id { get; init; }
/// <summary>
/// 修改者编号
/// </summary>
[Column(CanInsert = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual long? ModifiedUserId { get; init; }
/// <summary>
/// 修改者用户名
/// </summary>
[Column(DbType = Chars.FLG_DB_FIELD_TYPE_VARCHAR_31, CanInsert = false, Position = -1)]
[CsvIgnore]
[JsonIgnore]
public virtual string ModifiedUserName { get; init; }
}

View File

@ -0,0 +1,7 @@
namespace NetAdmin.Domain.DbMaps.Tpl;
/// <summary>
/// 示例表
/// </summary>
[Table(Name = Chars.FLG_DB_TABLE_NAME_PREFIX + nameof(Tpl_Example))]
public record Tpl_Example : VersionEntity;

View File

@ -0,0 +1,16 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 批量请求
/// </summary>
public sealed record BulkReq<T> : DataAbstraction
where T : DataAbstraction, new()
{
/// <summary>
/// 请求对象
/// </summary>
[MaxLength(Numbers.MAX_LIMIT_BULK_REQ)]
[MinLength(1)]
[Required(ErrorMessageResourceType = typeof(Ln), ErrorMessageResourceName = nameof(Ln.请求对象不能为空))]
public IEnumerable<T> Items { get; init; }
}

View File

@ -0,0 +1,14 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <inheritdoc cref="DelReq{T}" />
public sealed record DelReq : DelReq<long>;
/// <summary>
/// 请求:通过编号删除
/// </summary>
public record DelReq<T> : EntityBase<T>
where T : IEquatable<T>
{
/// <inheritdoc cref="EntityBase{T}.Id" />
public override T Id { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 信息:分页
/// </summary>
public interface IPagedInfo
{
/// <summary>
/// 当前页码
/// </summary>
int Page { get; init; }
/// <summary>
/// 页容量
/// </summary>
int PageSize { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 空请求
/// </summary>
public sealed record NopReq : DataAbstraction;

View File

@ -0,0 +1,16 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 请求:分页查询
/// </summary>
public sealed record PagedQueryReq<T> : QueryReq<T>, IPagedInfo
where T : DataAbstraction, new()
{
/// <inheritdoc cref="IPagedInfo.Page" />
[Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_NO)]
public int Page { get; init; } = 1;
/// <inheritdoc cref="IPagedInfo.PageSize" />
[Range(1, Numbers.MAX_LIMIT_QUERY_PAGE_SIZE)]
public int PageSize { get; init; } = Numbers.DEF_PAGE_SIZE_QUERY;
}

View File

@ -0,0 +1,24 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 响应:分页查询
/// </summary>
public sealed record PagedQueryRsp<T>(int Page, int PageSize, long Total, IEnumerable<T> Rows) : IPagedInfo
where T : DataAbstraction
{
/// <summary>
/// 数据行
/// </summary>
public IEnumerable<T> Rows { get; } = Rows;
/// <inheritdoc cref="IPagedInfo.Page" />
public int Page { get; init; } = Page;
/// <inheritdoc cref="IPagedInfo.PageSize" />
public int PageSize { get; init; } = PageSize;
/// <summary>
/// 数据总条
/// </summary>
public long Total { get; init; } = Total;
}

View File

@ -0,0 +1,72 @@
namespace NetAdmin.Domain.Dto.Dependency;
/// <summary>
/// 请求:查询
/// </summary>
public record QueryReq<T> : DataAbstraction
where T : DataAbstraction, new()
{
/// <summary>
/// 取前n条
/// </summary>
[Range(1, Numbers.MAX_LIMIT_QUERY)]
public int Count { get; init; } = Numbers.MAX_LIMIT_QUERY;
/// <summary>
/// 动态查询条件
/// </summary>
public DynamicFilterInfo DynamicFilter { get; init; }
/// <summary>
/// 查询条件
/// </summary>
public T Filter { get; init; }
/// <summary>
/// 查询关键字
/// </summary>
public string Keywords { get; init; }
/// <summary>
/// 排序方式
/// </summary>
public Orders? Order { get; init; }
/// <summary>
/// 排序字段
/// </summary>
public string Prop { get; init; }
/// <summary>
/// 所需字段
/// </summary>
public string[] RequiredFields { get; set; }
/// <summary>
/// 列表表达式
/// </summary>
public Expression<Func<TEntity, TEntity>> GetToListExp<TEntity>()
{
if (RequiredFields.NullOrEmpty()) {
return null;
}
var expParameter = Expression.Parameter(typeof(TEntity), "a");
var bindings = new List<MemberBinding>();
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var field in RequiredFields) {
var prop = typeof(TEntity).GetProperty(field);
if (prop == null || prop.GetCustomAttribute<DangerFieldAttribute>() != null) {
continue;
}
var propExp = Expression.Property(expParameter, prop);
var binding = Expression.Bind(prop, propExp);
bindings.Add(binding);
}
var expBody = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings);
return Expression.Lambda<Func<TEntity, TEntity>>(expBody, expParameter);
}
}

View File

@ -0,0 +1,17 @@
using NetAdmin.Domain.Enums;
namespace NetAdmin.Domain.Dto;
/// <summary>
/// 动态过滤条件生成器
/// </summary>
public sealed record DfBuilder
{
/// <summary>
/// 构建生成器
/// </summary>
public static DynamicFilterInfo New(DynamicFilterLogics logic)
{
return new DynamicFilterInfo { Logic = logic, Filters = [] };
}
}

View File

@ -0,0 +1,125 @@
using NetAdmin.Domain.Enums;
namespace NetAdmin.Domain.Dto;
/// <summary>
/// 动态过滤条件
/// </summary>
public sealed record DynamicFilterInfo : DataAbstraction
{
/// <summary>
/// 字段名
/// </summary>
public string Field { get; init; }
/// <summary>
/// 子过滤条件
/// </summary>
public List<DynamicFilterInfo> Filters { get; init; }
/// <summary>
/// 子过滤条件逻辑关系
/// </summary>
public DynamicFilterLogics Logic { get; init; }
/// <summary>
/// 操作符
/// </summary>
public DynamicFilterOperators Operator { get; init; }
/// <summary>
/// 值
/// </summary>
public object Value { get; init; }
/// <summary>
/// 隐式转换为 FreeSql 的 DynamicFilterInfo 对象
/// </summary>
public static implicit operator FreeSql.Internal.Model.DynamicFilterInfo(DynamicFilterInfo d)
{
var ret = d.Adapt<FreeSql.Internal.Model.DynamicFilterInfo>();
ProcessDynamicFilter(ret);
return ret;
}
/// <summary>
/// 添加子过滤条件
/// </summary>
public DynamicFilterInfo Add(DynamicFilterInfo df)
{
if (Filters == null) {
return this with { Filters = [df] };
}
Filters.Add(df);
return this;
}
/// <summary>
/// 添加过滤条件
/// </summary>
public DynamicFilterInfo Add(string field, DynamicFilterOperators opt, object val)
{
return Add(new DynamicFilterInfo { Field = field, Operator = opt, Value = val });
}
/// <summary>
/// 添加过滤条件
/// </summary>
public DynamicFilterInfo AddIf(bool condition, string field, DynamicFilterOperators opt, object val)
{
return !condition ? this : Add(field, opt, val);
}
/// <summary>
/// 添加过滤条件
/// </summary>
public DynamicFilterInfo AddIf(bool condition, DynamicFilterInfo df)
{
return !condition ? this : Add(df);
}
private static void ParseDateExp(FreeSql.Internal.Model.DynamicFilterInfo d)
{
var values = ((JsonElement)d.Value).Deserialize<string[]>();
if (!DateTime.TryParse(values[0], CultureInfo.InvariantCulture, out _)) {
var result = values[0]
.ExecuteCSharpCodeAsync<DateTime>([typeof(DateTime).Assembly], nameof(System))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
values[0] = $"{result:yyyy-MM-dd HH:mm:ss}";
}
if (!DateTime.TryParse(values[1], CultureInfo.InvariantCulture, out _)) {
var result = values[1]
.ExecuteCSharpCodeAsync<DateTime>([typeof(DateTime).Assembly], nameof(System))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
values[1] = $"{result:yyyy-MM-dd HH:mm:ss}";
}
d.Value = values;
}
private static void ProcessDynamicFilter(FreeSql.Internal.Model.DynamicFilterInfo d)
{
if (d?.Filters != null) {
foreach (var filterInfo in d.Filters) {
ProcessDynamicFilter(filterInfo);
}
}
if (new[] { nameof(IFieldCreatedClientIp.CreatedClientIp), nameof(IFieldModifiedClientIp.ModifiedClientIp) }.Contains(
d?.Field, StringComparer.OrdinalIgnoreCase)) {
var val = d!.Value?.ToString();
if (val?.IsIpV4() == true) {
d.Value = val.IpV4ToInt32();
}
}
else if (d?.Operator == DynamicFilterOperator.DateRange) {
ParseDateExp(d);
}
}
}

View File

@ -0,0 +1,25 @@
namespace NetAdmin.Domain.Dto;
/// <summary>
/// 信息RESTful 风格结果集
/// </summary>
public record RestfulInfo<T> : DataAbstraction
{
/// <summary>
/// 代码
/// </summary>
/// <example>succeed</example>
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public ErrorCodes Code { get; init; }
/// <summary>
/// 数据
/// </summary>
public T Data { get; init; }
/// <summary>
/// 字符串:"消息内容",或数组:[{"参数名1":"消息内容1"},{"参数名2":"消息内容2"}]
/// </summary>
/// <example>请求成功</example>
public object Msg { get; init; }
}

View File

@ -0,0 +1,8 @@
using NetAdmin.Domain.DbMaps.Tpl;
namespace NetAdmin.Domain.Dto.Tpl.Example;
/// <summary>
/// 请求:创建示例
/// </summary>
public record CreateExampleReq : Tpl_Example;

View File

@ -0,0 +1,13 @@
using NetAdmin.Domain.DbMaps.Tpl;
namespace NetAdmin.Domain.Dto.Tpl.Example;
/// <summary>
/// 请求:查询示例
/// </summary>
public sealed record QueryExampleReq : Tpl_Example
{
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
}

View File

@ -0,0 +1,17 @@
using NetAdmin.Domain.DbMaps.Tpl;
namespace NetAdmin.Domain.Dto.Tpl.Example;
/// <summary>
/// 响应:查询示例
/// </summary>
public sealed record QueryExampleRsp : Tpl_Example
{
/// <inheritdoc cref="EntityBase{T}.Id" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Id { get; init; }
/// <inheritdoc cref="IFieldVersion.Version" />
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override long Version { get; init; }
}

View File

@ -0,0 +1,22 @@
namespace NetAdmin.Domain.Enums;
/// <summary>
/// 动态查询条件逻辑运算符
/// </summary>
[Export]
public enum DynamicFilterLogics
{
/// <summary>
/// 并且
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.并且))]
And = 0
,
/// <summary>
/// 或者
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.或者))]
Or = 1
}

View File

@ -0,0 +1,158 @@
namespace NetAdmin.Domain.Enums;
/// <summary>
/// 动态查询条件操作符
/// </summary>
[Export]
public enum DynamicFilterOperators
{
/// <summary>
/// 包含
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.包含))]
Contains = 0
,
/// <summary>
/// 以什么开始
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.以什么开始))]
StartsWith = 1
,
/// <summary>
/// 以什么结束
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.以什么结束))]
EndsWith = 2
,
/// <summary>
/// 不包含
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.不包含))]
NotContains = 3
,
/// <summary>
/// 不以什么开始
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.不以什么开始))]
NotStartsWith = 4
,
/// <summary>
/// 不以什么结束
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.不以什么结束))]
NotEndsWith = 5
,
/// <summary>
/// 等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.等于))]
Equal = 6
,
/// <summary>
/// 等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.等于))]
Equals = 7
,
/// <summary>
/// 等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.等于))]
Eq = 8
,
/// <summary>
/// 不等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.不等于))]
NotEqual = 9
,
/// <summary>
/// 大于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.大于))]
GreaterThan = 10
,
/// <summary>
/// 大于等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.大于等于))]
GreaterThanOrEqual = 11
,
/// <summary>
/// 小于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.小于))]
LessThan = 12
,
/// <summary>
/// 小于等于
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.小于等于))]
LessThanOrEqual = 13
,
/// <summary>
/// 范围
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.范围))]
Range = 14
,
/// <summary>
/// 日期范围
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.日期范围))]
DateRange = 15
,
/// <summary>
/// 为其中之一
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.为其中之一))]
Any = 16
,
/// <summary>
/// 不为其中之一
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.不为其中之一))]
NotAny = 17
,
/// <summary>
/// 自定义
/// </summary>
[ResourceDescription<Ln>(nameof(Ln.自定义))]
Custom = 18
}

View File

@ -0,0 +1,69 @@
namespace NetAdmin.Domain.Enums;
/// <summary>
/// HTTP 请求方法
/// </summary>
[Export]
public enum HttpMethods
{
/// <summary>
/// Connect
/// </summary>
Connect = 1
,
/// <summary>
/// Delete
/// </summary>
Delete = 2
,
/// <summary>
/// Get
/// </summary>
Get = 3
,
/// <summary>
/// Head
/// </summary>
Head = 4
,
/// <summary>
/// Options
/// </summary>
Options = 5
,
/// <summary>
/// Patch
/// </summary>
Patch = 6
,
/// <summary>
/// Post
/// </summary>
Post = 7
,
/// <summary>
/// Put
/// </summary>
Put = 8
,
/// <summary>
/// Trace
/// </summary>
Trace = 9
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// 泛型事件源接口
/// </summary>
public interface IEventSourceGeneric<out T> : IEventSource
{
/// <summary>
/// 事件承载(携带)数据
/// </summary>
T Data { get; }
}

View File

@ -0,0 +1,38 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// 种子数据插入完毕事件
/// </summary>
public sealed record SeedDataInsertedEvent : DataAbstraction, IEventSource
{
/// <summary>
/// Initializes a new instance of the <see cref="SeedDataInsertedEvent" /> class.
/// </summary>
public SeedDataInsertedEvent(int insertedCount, bool isConsumOnce = false)
{
IsConsumOnce = isConsumOnce;
InsertedCount = insertedCount;
CreatedTime = DateTime.Now;
EventId = nameof(SeedDataInsertedEvent);
}
/// <inheritdoc />
public DateTime CreatedTime { get; }
/// <inheritdoc />
public string EventId { get; }
/// <inheritdoc />
public bool IsConsumOnce { get; }
/// <inheritdoc />
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// 插入数量
/// </summary>
public int InsertedCount { get; set; }
/// <inheritdoc />
public object Payload { get; init; }
}

View File

@ -0,0 +1,29 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// Sql命令执行后事件
/// </summary>
public sealed record SqlCommandAfterEvent : SqlCommandBeforeEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="SqlCommandAfterEvent" /> class.
/// </summary>
public SqlCommandAfterEvent(CommandAfterEventArgs e) //
: base(e)
{
ElapsedMilliseconds = (long)((double)e.ElapsedTicks / Stopwatch.Frequency * 1_000);
EventId = nameof(SqlCommandAfterEvent);
}
/// <summary>
/// 耗时(单位:毫秒)
/// </summary>
/// de
private long ElapsedMilliseconds { get; }
/// <inheritdoc />
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: {2} ms {1}", Id, Sql, ElapsedMilliseconds);
}
}

View File

@ -0,0 +1,24 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// Sql命令执行前事件
/// </summary>
public record SqlCommandBeforeEvent : SqlCommandEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="SqlCommandBeforeEvent" /> class.
/// </summary>
public SqlCommandBeforeEvent(CommandBeforeEventArgs e)
{
Identifier = e.Identifier;
Sql = e.Command.ParameterFormat().RemoveWrapped();
EventId = nameof(SqlCommandBeforeEvent);
CreatedTime = DateTime.Now;
}
/// <inheritdoc />
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "SQL-{0}: Executing...", Id);
}
}

View File

@ -0,0 +1,45 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// Sql命令事件
/// </summary>
public abstract record SqlCommandEvent : DataAbstraction, IEventSource
{
/// <summary>
/// Initializes a new instance of the <see cref="SqlCommandEvent" /> class.
/// </summary>
protected SqlCommandEvent(bool isConsumOnce = false)
{
IsConsumOnce = isConsumOnce;
}
/// <inheritdoc />
public bool IsConsumOnce { get; }
/// <inheritdoc />
public CancellationToken CancellationToken { get; init; }
/// <inheritdoc />
public DateTime CreatedTime { get; protected init; }
/// <inheritdoc />
public string EventId { get; protected init; }
/// <inheritdoc />
public object Payload { get; init; }
/// <summary>
/// 标识符缩写
/// </summary>
protected string Id => Identifier.ToString()[..8].ToUpperInvariant();
/// <summary>
/// 标识符,可将 CommandBefore 与 CommandAfter 进行匹配
/// </summary>
protected Guid Identifier { get; init; }
/// <summary>
/// 关联的Sql语句
/// </summary>
protected string Sql { get; init; }
}

View File

@ -0,0 +1,22 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// 同步数据库结构之后事件
/// </summary>
public sealed record SyncStructureAfterEvent : SyncStructureBeforeEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncStructureAfterEvent" /> class.
/// </summary>
public SyncStructureAfterEvent(SyncStructureBeforeEventArgs e) //
: base(e)
{
EventId = nameof(SyncStructureAfterEvent);
}
/// <inheritdoc />
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}: {1}: {2}", Id, Ln., string.Join(',', EntityTypes.Select(x => x.Name)));
}
}

View File

@ -0,0 +1,29 @@
namespace NetAdmin.Domain.Events;
/// <summary>
/// 同步数据库结构之前事件
/// </summary>
public record SyncStructureBeforeEvent : SqlCommandEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncStructureBeforeEvent" /> class.
/// </summary>
public SyncStructureBeforeEvent(SyncStructureBeforeEventArgs e)
{
Identifier = e.Identifier;
EventId = nameof(SyncStructureBeforeEvent);
CreatedTime = DateTime.Now;
EntityTypes = e.EntityTypes;
}
/// <summary>
/// 实体类型
/// </summary>
protected Type[] EntityTypes { get; }
/// <inheritdoc />
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}: {1}", Id, Ln.);
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)/build/code.quality.props"/>
<ItemGroup>
<ProjectReference Include="../NetAdmin.Infrastructure/NetAdmin.Infrastructure.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CronExpressionDescriptor" Version="2.36.0"/>
<PackageReference Include="Cronos" Version="0.8.4"/>
<PackageReference Include="CsvHelper.NS" Version="33.0.2-ns2"/>
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,5 @@
global using NetAdmin.Domain.Attributes;
global using NetAdmin.Domain.DbMaps.Dependency;
global using NetAdmin.Domain.DbMaps.Dependency.Fields;
global using CsvIgnore = CsvHelper.Configuration.Attributes.IgnoreAttribute;
global using DynamicFilterOperators = NetAdmin.Domain.Enums.DynamicFilterOperators;

View File

@ -0,0 +1,7 @@
namespace NetAdmin.Host.Attributes;
/// <summary>
/// 标记一个Action其响应的json结果会被删除值为null的节点
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class RemoveNullNodeAttribute : Attribute;

View File

@ -0,0 +1,41 @@
namespace NetAdmin.Host.Attributes;
/// <summary>
/// 标记一个Action启用事务
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class TransactionAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="TransactionAttribute" /> class.
/// </summary>
public TransactionAttribute() { }
/// <summary>
/// Initializes a new instance of the <see cref="TransactionAttribute" /> class.
/// </summary>
public TransactionAttribute(Propagation propagation) //
: this(null, propagation) { }
/// <summary>
/// Initializes a new instance of the <see cref="TransactionAttribute" /> class.
/// </summary>
public TransactionAttribute(IsolationLevel isolationLevel, Propagation propagation) //
: this(new IsolationLevel?(isolationLevel), propagation) { }
private TransactionAttribute(IsolationLevel? isolationLevel, Propagation propagation)
{
IsolationLevel = isolationLevel;
Propagation = propagation;
}
/// <summary>
/// 事务隔离级别
/// </summary>
public IsolationLevel? IsolationLevel { get; }
/// <summary>
/// 事务传播方式
/// </summary>
public Propagation Propagation { get; } = Propagation.Required;
}

View File

@ -0,0 +1,12 @@
namespace NetAdmin.Host.BackgroundRunning;
/// <summary>
/// 轮询工作接口
/// </summary>
public interface IPollingWork
{
/// <summary>
/// 启动工作
/// </summary>
ValueTask StartAsync(CancellationToken cancelToken);
}

View File

@ -0,0 +1,15 @@
using NetAdmin.Domain;
namespace NetAdmin.Host.BackgroundRunning;
/// <summary>
/// 轮询工作
/// </summary>
public abstract class PollingWork<TWorkData>(TWorkData workData) : WorkBase<TWorkData>
where TWorkData : DataAbstraction
{
/// <summary>
/// 工作数据
/// </summary>
protected TWorkData WorkData => workData;
}

View File

@ -0,0 +1,75 @@
using StackExchange.Redis;
namespace NetAdmin.Host.BackgroundRunning;
/// <summary>
/// 工作基类
/// </summary>
public abstract class WorkBase<TLogger>
{
/// <summary>
/// Initializes a new instance of the <see cref="WorkBase{TLogger}" /> class.
/// </summary>
protected WorkBase()
{
ServiceProvider = App.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
UowManager = ServiceProvider.GetService<UnitOfWorkManager>();
Logger = ServiceProvider.GetService<ILogger<TLogger>>();
}
/// <summary>
/// 日志记录器
/// </summary>
protected ILogger<TLogger> Logger { get; }
/// <summary>
/// 服务提供器
/// </summary>
protected IServiceProvider ServiceProvider { get; }
/// <summary>
/// 事务单元管理器
/// </summary>
protected UnitOfWorkManager UowManager { get; }
/// <summary>
/// 通用工作流
/// </summary>
protected abstract ValueTask WorkflowAsync( //
// ReSharper disable once UnusedParameter.Global
#pragma warning disable SA1114
CancellationToken cancelToken);
#pragma warning restore SA1114
/// <summary>
/// 通用工作流
/// </summary>
/// <exception cref="NetAdminGetLockerException">加锁失败异常</exception>
protected async ValueTask WorkflowAsync(bool singleInstance, CancellationToken cancelToken)
{
if (singleInstance) {
// 加锁
var lockName = GetType().FullName;
await using var redisLocker = await GetLockerAsync(lockName).ConfigureAwait(false);
await WorkflowAsync(cancelToken).ConfigureAwait(false);
return;
}
await WorkflowAsync(cancelToken).ConfigureAwait(false);
}
/// <summary>
/// 获取锁
/// </summary>
private Task<RedisLocker> GetLockerAsync(string lockId)
{
var db = ServiceProvider.GetService<IConnectionMultiplexer>()
.GetDatabase(ServiceProvider.GetService<IOptions<RedisOptions>>()
.Value.Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE)
.Database);
return RedisLocker.GetLockerAsync(db, lockId, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_EXPIRY), Numbers.MAX_LIMIT_RETRY_CNT_REDIS_LOCK
, TimeSpan.FromSeconds(Numbers.SECS_REDIS_LOCK_RETRY_DELAY));
}
}

View File

@ -0,0 +1,17 @@
using NetAdmin.Application.Services;
using NetAdmin.Cache;
namespace NetAdmin.Host.Controllers;
/// <summary>
/// 控制器基类
/// </summary>
public abstract class ControllerBase<TCache, TService>(TCache cache = default) : IDynamicApiController
where TCache : ICache<IDistributedCache, TService> //
where TService : IService
{
/// <summary>
/// 关联的缓存
/// </summary>
protected TCache Cache => cache;
}

View File

@ -0,0 +1,98 @@
using NetAdmin.Application.Services;
using NetAdmin.Cache;
using NetAdmin.Host.Middlewares;
namespace NetAdmin.Host.Controllers;
/// <summary>
/// 探针组件
/// </summary>
[ApiDescriptionSettings("Probe")]
[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
public sealed class ProbeController : ControllerBase<ICache<IDistributedCache, IService>, IService>
{
/// <summary>
/// 退出程序
/// </summary>
[AllowAnonymous]
[HttpGet]
#pragma warning disable CA1822
public ActionResult Exit(string token)
#pragma warning restore CA1822
{
if (token != GlobalStatic.SecretKey) {
return new UnauthorizedResult();
}
Environment.Exit(0);
return new OkResult();
}
/// <summary>
/// 健康检查
/// </summary>
[AllowAnonymous]
[HttpGet]
#pragma warning disable CA1822, S3400
public object HealthCheck()
#pragma warning restore S3400, CA1822
{
return new {
HostName = Environment.MachineName
, CurrentConnections = SafetyShopHostMiddleware.Connections
, GlobalStatic.ProductVersion
, ThreadCounts = GlobalStatic.CurrentProcess.Threads.Count
, GlobalStatic.LatestLogTime
};
}
/// <summary>
/// 系统是否已经安全停止
/// </summary>
[AllowAnonymous]
[HttpGet]
[NonUnify]
#pragma warning disable CA1822, S3400
public IActionResult IsSystemSafetyStopped(int logTimeoutSeconds = 15)
#pragma warning restore S3400, CA1822
{
return new ContentResult { Content = (DateTime.Now - GlobalStatic.LatestLogTime).TotalSeconds > logTimeoutSeconds ? "1" : "0" };
}
/// <summary>
/// 实例下线
/// </summary>
/// <remarks>
/// 流量只出不进
/// </remarks>
[AllowAnonymous]
[HttpGet]
#pragma warning disable CA1822
public ActionResult Offline(string token)
#pragma warning restore CA1822
{
if (token != GlobalStatic.SecretKey) {
return new UnauthorizedResult();
}
SafetyShopHostMiddleware.Stop();
return new OkResult();
}
/// <summary>
/// 停止日志计数器
/// </summary>
[AllowAnonymous]
[HttpGet]
#pragma warning disable CA1822
public ActionResult StopLogCounter(string token)
#pragma warning restore CA1822
{
if (token != GlobalStatic.SecretKey) {
return new UnauthorizedResult();
}
GlobalStatic.LogCounterOff = true;
return new OkResult();
}
}

View File

@ -0,0 +1,94 @@
using NetAdmin.Application.Modules.Tpl;
using NetAdmin.Application.Services.Tpl.Dependency;
using NetAdmin.Cache.Tpl.Dependency;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Tpl.Example;
using NetAdmin.Host.Attributes;
namespace NetAdmin.Host.Controllers.Tpl;
/// <summary>
/// 示例服务
/// </summary>
[ApiDescriptionSettings(nameof(Tpl), Module = nameof(Tpl))]
[Produces(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON)]
public sealed class ExampleController(IExampleCache cache) : ControllerBase<IExampleCache, IExampleService>(cache), IExampleModule
{
/// <summary>
/// 批量删除示例
/// </summary>
[Transaction]
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
return Cache.BulkDeleteAsync(req);
}
/// <summary>
/// 示例计数
/// </summary>
public Task<long> CountAsync(QueryReq<QueryExampleReq> req)
{
return Cache.CountAsync(req);
}
/// <summary>
/// 创建示例
/// </summary>
[Transaction]
public Task<QueryExampleRsp> CreateAsync(CreateExampleReq req)
{
return Cache.CreateAsync(req);
}
/// <summary>
/// 删除示例
/// </summary>
[Transaction]
public Task<int> DeleteAsync(DelReq req)
{
return Cache.DeleteAsync(req);
}
/// <summary>
/// 示例是否存在
/// </summary>
[NonAction]
public Task<bool> ExistAsync(QueryReq<QueryExampleReq> req)
{
return Cache.ExistAsync(req);
}
/// <summary>
/// 导出示例
/// </summary>
[NonAction]
public Task<IActionResult> ExportAsync(QueryReq<QueryExampleReq> req)
{
return Cache.ExportAsync(req);
}
/// <summary>
/// 获取单个示例
/// </summary>
public Task<QueryExampleRsp> GetAsync(QueryExampleReq req)
{
return Cache.GetAsync(req);
}
/// <summary>
/// 分页查询示例
/// </summary>
public Task<PagedQueryRsp<QueryExampleRsp>> PagedQueryAsync(PagedQueryReq<QueryExampleReq> req)
{
return Cache.PagedQueryAsync(req);
}
/// <summary>
/// 查询示例
/// </summary>
[NonAction]
public Task<IEnumerable<QueryExampleRsp>> QueryAsync(QueryReq<QueryExampleReq> req)
{
return Cache.QueryAsync(req);
}
}

View File

@ -0,0 +1,34 @@
namespace NetAdmin.Host.Extensions;
/// <summary>
/// HttpContext 扩展方法
/// </summary>
public static class HttpContextExtensions
{
private static readonly Regex _nullRegex = new("\"[^\"]+?\":null,?", RegexOptions.Compiled);
/// <summary>
/// 删除 response json body 中value 为null的节点
/// </summary>
public static async Task RemoveJsonNodeWithNullValueAsync(this HttpContext me)
{
// 非json格式退出
if (!(me.Response.ContentType?.Contains(Chars.FLG_HTTP_HEADER_VALUE_APPLICATION_JSON) ?? false)) {
return;
}
// 流不可读,退出
if (!me.Response.Body.CanSeek || !me.Response.Body.CanRead || !me.Response.Body.CanWrite) {
return;
}
_ = me.Response.Body.Seek(0, SeekOrigin.Begin);
var sr = new StreamReader(me.Response.Body);
var bodyString = await sr.ReadToEndAsync().ConfigureAwait(false);
bodyString = _nullRegex.Replace(bodyString, string.Empty).Replace(",}", "}");
_ = me.Response.Body.Seek(0, SeekOrigin.Begin);
var bytes = Encoding.UTF8.GetBytes(bodyString);
me.Response.Body.SetLength(bytes.Length);
await me.Response.Body.WriteAsync(bytes).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,62 @@
#if DEBUG
using IGeekFan.AspNetCore.Knife4jUI;
#else
using Prometheus;
using Prometheus.HttpMetrics;
#endif
namespace NetAdmin.Host.Extensions;
/// <summary>
/// ApplicationBuilder对象 扩展方法
/// </summary>
[SuppressSniffer]
// ReSharper disable once InconsistentNaming
public static class IApplicationBuilderExtensions
{
/// <summary>
/// 执行匹配的端点
/// </summary>
public static IApplicationBuilder UseEndpoints(this IApplicationBuilder me)
{
return me.UseEndpoints(endpoints => {
_ = endpoints.MapControllers();
#if !DEBUG
_ = endpoints.MapMetrics();
#endif
});
}
#if DEBUG
/// <summary>
/// 使用 api skin knife4j-vue
/// </summary>
public static IApplicationBuilder UseOpenApiSkin(this IApplicationBuilder me)
{
return me.UseKnife4UI(options => {
options.RoutePrefix = string.Empty; // 配置 Knife4UI 路由地址
foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) {
options.SwaggerEndpoint(groupInfo.RouteTemplate, groupInfo.Title);
}
});
}
#else
/// <summary>
/// 使用 Prometheus
/// </summary>
public static IApplicationBuilder UsePrometheus(this IApplicationBuilder me)
{
return me.UseHttpMetrics(opt => {
opt.RequestDuration.Histogram = Metrics.CreateHistogram( //
"http_request_duration_seconds"
, "The duration of HTTP requests processed by an ASP.NET Core application."
, HttpRequestLabelNames.All
, new HistogramConfiguration {
Buckets = Histogram.PowersOfTenDividedBuckets(
-2, 2, 4)
});
});
}
#endif
}

View File

@ -0,0 +1,115 @@
using NetAdmin.Host.Filters;
using NetAdmin.Host.Utils;
namespace NetAdmin.Host.Extensions;
/// <summary>
/// IMvcBuilder 扩展方法
/// </summary>
[SuppressSniffer]
// ReSharper disable once InconsistentNaming
public static class IMvcBuilderExtensions
{
/// <summary>
/// api结果处理器
/// </summary>
public static IMvcBuilder AddDefaultApiResultHandler(this IMvcBuilder me)
{
return me.AddInjectWithUnifyResult<DefaultApiResultHandler>(injectOptions => {
injectOptions.ConfigureSwaggerGen(genOptions => {
// 替换自定义的EnumSchemaFilter支持多语言Resx资源 需将SpecificationDocumentSettings.EnableEnumSchemaFilter配置为false)
genOptions.SchemaFilter<SwaggerEnumSchemaFixer>();
// 枚举显示自身xml comment 而不是$ref原型引用
genOptions.UseInlineDefinitionsForEnums();
// 将程序集版本号与OpenApi版本号同步
foreach (var doc in genOptions.SwaggerGeneratorOptions.SwaggerDocs) {
doc.Value.Version = FileVersionInfo
.GetVersionInfo(Assembly.GetEntryAssembly()!.Location)
.ProductVersion;
}
});
});
}
/// <summary>
/// Json序列化配置
/// </summary>
/// <remarks>
/// 正反序列化规则:
/// object->json
/// 1、值为 null 或 default 的节点将被忽略
/// 2、值为 "" 的节点将被忽略。
/// 3、值为 [] 的节点将被忽略。
/// 4、节点名大驼峰转小驼峰
/// 5、不转义除对json结构具有破坏性如")以外的任何字符
/// 6、大数字原样输出不加引号由前端处理js大数兼容问题
/// json->object:
/// 1、允许带注释的json自动忽略
/// 2、允许尾随逗号
/// 3、节点名大小写不敏感
/// 4、允许带双引号的数字
/// 5、值为"" 转 null
/// 6、值为[] 转 null
/// </remarks>
public static IMvcBuilder AddJsonSerializer(this IMvcBuilder me, bool enumToString = false)
{
return me.AddJsonOptions(options => SetJsonOptions(enumToString, options));
}
/// <summary>
/// 设置Json选项
/// </summary>
private static void SetJsonOptions(bool enumToString, JsonOptions options)
{
////////////////////////////// json -> object
// 允许带注释
options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
// 允许尾随逗号
options.JsonSerializerOptions.AllowTrailingCommas = true;
// 允许数字带双引号
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
// 大小写不敏感
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
// 允许读取引号包围的数字
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
///////////////////////////// object -> json
// 转小驼峰
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
// 不严格转义
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
// 写入时忽略null、default
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
////////////////////////////// object <-> json
// "" 转 null 双向
options.JsonSerializerOptions.Converters.Add(new ToNullIfEmptyStringConverter());
// [] 转 null 双向
options.JsonSerializerOptions.TypeInfoResolver = new CollectionJsonTypeInfoResolver();
// 日期格式 2023-01-18 20:02:12
_ = options.JsonSerializerOptions.Converters.AddDateTimeTypeConverters();
// object->json 枚举显名 而非数字 json->object 可以枚举名 也可以数值
if (enumToString) {
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
}
// 快捷访问方式
GlobalStatic.JsonSerializerOptions = options.JsonSerializerOptions;
}
}

View File

@ -0,0 +1,18 @@
namespace NetAdmin.Host.Extensions;
/// <summary>
/// Type 扩展方法
/// </summary>
public static class MethodInfoExtensions
{
/// <summary>
/// 获取路由路径
/// </summary>
public static string GetRoutePath(this MethodInfo me, IServiceProvider serviceProvider)
{
return serviceProvider.GetService<IActionDescriptorCollectionProvider>()
.ActionDescriptors.Items.FirstOrDefault(x => x.DisplayName!.StartsWith( //
$"{me.DeclaringType}.{me.Name}", StringComparison.Ordinal))
?.AttributeRouteInfo?.Template;
}
}

View File

@ -0,0 +1,19 @@
using NetAdmin.Domain.Dto;
namespace NetAdmin.Host.Extensions;
/// <summary>
/// ResourceExecutingContextExtensions
/// </summary>
public static class ResourceExecutingContextExtensions
{
/// <summary>
/// 设置失败结果
/// </summary>
public static void SetFailResult<T>(this ResourceExecutingContext me, ErrorCodes errorCode, string errorMsg = null)
where T : RestfulInfo<object>, new()
{
me.Result = new JsonResult(new T { Code = errorCode, Msg = errorMsg });
me.HttpContext.Response.StatusCode = Numbers.HTTP_STATUS_BIZ_FAIL;
}
}

View File

@ -0,0 +1,216 @@
using Gurion.Logging;
using NetAdmin.Domain.Contexts;
using StackExchange.Redis;
using Yitter.IdGenerator;
#if DEBUG
using Spectre.Console;
#endif
namespace NetAdmin.Host.Extensions;
/// <summary>
/// ServiceCollection 扩展方法
/// </summary>
[SuppressSniffer]
public static class ServiceCollectionExtensions
{
#if DEBUG
private static readonly Dictionary<Regex, string> _consoleColors //
= new() {
{
new Regex( //
@"(\d{2,}\.\d+ ?ms)", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Magenta)}]$1[/]"
}
, {
new Regex( //
"(Tb[a-zA-Z0-9]+)", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Cyan)}]$1[/]"
}
, {
new Regex( //
"(INSERT) ", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Blue)}]$1[/] "
}
, {
new Regex( //
"(SELECT) ", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Green)}]$1[/] "
}
, {
new Regex( //
"(UPDATE) ", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Yellow)}]$1[/] "
}
, {
new Regex( //
"(DELETE) ", RegexOptions.Compiled)
, $"[{nameof(ConsoleColor.Red)}]$1[/] "
}
, {
new Regex( //
"(<s:.+?>)", RegexOptions.Compiled)
, $"[underline {nameof(ConsoleColor.Gray)}]$1[/] "
}
, {
new Regex( //
"(ResponseBody)", RegexOptions.Compiled)
, $"[underline {nameof(ConsoleColor.Cyan)}]$1[/] "
}
, {
new Regex( //
"(RequestBody)", RegexOptions.Compiled)
, $"[underline {nameof(ConsoleColor.Magenta)}]$1[/] "
}
, {
new Regex( //
@"(\[\[dbo\]\]\.)(\[\[.+?\]\]) ", RegexOptions.Compiled)
, $"$1[{nameof(ConsoleColor.Magenta)}]$2[/] "
}
};
#endif
/// <summary>
/// 扫描程序集中继承自IConfigurableOptions的选项注册
/// </summary>
public static IServiceCollection AddAllOptions( //
this IServiceCollection me)
{
var optionsTypes
= from type in App.EffectiveTypes.Where(x => !x.IsAbstract && !x.FullName!.Contains(nameof(Gurion)) &&
x.GetInterfaces().Contains(typeof(IConfigurableOptions)))
select type;
var sbLog = new StringBuilder();
foreach (var type in optionsTypes) {
var configureMethod = typeof(ConfigurableOptionsServiceCollectionExtensions).GetMethod(
nameof(ConfigurableOptionsServiceCollectionExtensions.AddConfigurableOptions), BindingFlags.Public | BindingFlags.Static
, [typeof(IServiceCollection)]);
_ = configureMethod!.MakeGenericMethod(type).Invoke(me, [me]);
_ = sbLog.Append(CultureInfo.InvariantCulture, $" {type.Name}");
}
LogHelper.Get<IServiceCollection>()?.Info($"{Ln.配置文件初始化完毕} {sbLog}");
return me;
}
/// <summary>
/// 添加控制台日志模板
/// </summary>
public static IServiceCollection AddConsoleFormatter(this IServiceCollection me)
{
return me.AddConsoleFormatter(options => {
var logLevels = Enum.GetValues<LogLevels>().ToDictionary(x => x, x => x.GetDisplay());
options.WriteHandler = (message, _, _, _, _) => {
#if DEBUG
MarkupLine(message.Message.EscapeMarkup(), message, logLevels);
if (message.Exception != null) {
MarkupLine(message.Exception.ToString().EscapeMarkup(), message, logLevels);
}
#else
var msg = message.Message.ReplaceLineEndings(string.Empty);
var (date, logName, logFormat) = ParseMessage(message, false);
Console.WriteLine( //
logFormat, date, logLevels[(LogLevels)message.LogLevel].ShortName, logName, message.ThreadId, msg);
#endif
GlobalStatic.IncrementLogCounter();
};
});
}
/// <summary>
/// 添加上下文用户令牌
/// </summary>
public static IServiceCollection AddContextUserToken(this IServiceCollection me)
{
return me.AddScoped(typeof(ContextUserToken), _ => ContextUserToken.Create());
}
/// <summary>
/// 添加事件总线
/// </summary>
public static IServiceCollection AddEventBus(this IServiceCollection me)
{
return me.AddEventBus(builder => builder.AddSubscribers(App.Assemblies.ToArray()));
}
/// <summary>
/// 添加内存缓存
/// </summary>
public static IServiceCollection AddMemCache(this IServiceCollection me)
{
return me.AddMemoryCache(options => options.TrackStatistics = true);
}
/// <summary>
/// OpenTelemetry数据监控
/// </summary>
public static IServiceCollection AddOpenTelemetryNet(this IServiceCollection me)
{
// _ = me.AddOpenTelemetry()
// .WithMetrics(builder => builder.AddAspNetCoreInstrumentation()
// .AddHttpClientInstrumentation()
// .AddRuntimeInstrumentation()
// .AddProcessInstrumentation()
// .AddPrometheusExporter());
return me;
}
/// <summary>
/// 添加 Redis缓存
/// </summary>
public static IServiceCollection AddRedisCache(this IServiceCollection me)
{
var redisOptions = App.GetOptions<RedisOptions>().Instances.First(x => x.Name == Chars.FLG_REDIS_INSTANCE_DATA_CACHE);
// IDistributedCache 分布式缓存通用接口
_ = me.AddStackExchangeRedisCache(options => {
// 连接字符串
options.Configuration = redisOptions.ConnStr;
});
// Redis原生接口
return me.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisOptions.ConnStr));
}
/// <summary>
/// 添加雪花编号生成器
/// </summary>
public static IServiceCollection AddSnowflake(this IServiceCollection me)
{
// 雪花漂移算法
var workerId = Environment.GetEnvironmentVariable(Chars.FLG_SNOWFLAKE_WORK_ID).Int32Try(0);
var idGeneratorOptions = new IdGeneratorOptions((ushort)workerId) { WorkerIdBitLength = 6 };
YitIdHelper.SetIdGenerator(idGeneratorOptions);
return me;
}
#if DEBUG
private static void MarkupLine( //
string msg //
, LogMessage message //
, Dictionary<LogLevels, DisplayAttribute> logLevels)
{
msg = _consoleColors.Aggregate( //
msg, (current, regex) => regex.Key.Replace(current, regex.Value));
msg = msg.ReplaceLineEndings(string.Empty);
var colorName = logLevels[(LogLevels)message.LogLevel].Name!;
var (date, logName, logFormat) = ParseMessage(message, true);
AnsiConsole.MarkupLine( //
CultureInfo.InvariantCulture, logFormat, date, colorName, logName, message.ThreadId, msg);
}
#endif
private static (string Date, string LogName, string LogFormat) ParseMessage(LogMessage message, bool showColor)
{
var date = $"{message.LogDateTime:HH:mm:ss.ffffff}";
var logName = message.LogName.PadRight(64, ' ')[^64..];
var format = showColor
? $"[{nameof(ConsoleColor.Gray)}][[{{0}} {{1}} {{2,-{64}}} #{{3,4}}]][/] {{4}}"
: $"[{{0}} {{1}} {{2,-{64}}} #{{3,4}}] {{4}}";
return (date, logName, format);
}
}

Some files were not shown because too many files have changed in this diff Show More