注意:目前v6.2.1
版本已弃用,请使用基于.Net8的v8.1.1
版本,最后更新于2024-07-07
本文档提供了Dakua快速开发框架的相关开发指南,如有问题请联系tonydai:6000666@qq.com
本框架亮点:
.Net 6
的跨平台设计(商业应用已长期运行在CentOS
及Windows
平台下,以及Docker
环境中)MySQL
、SqlServer
、Oracle
、PostgreSql
)EF
、SqlSugar
Vue2 + Element
及Vue3 + TS + ElementPlus
的前端框架
MediatR
)IAcModule
UseNacosConfiguration(x => x.AddSerilog(Log.Logger))
UseCommFileConfiguration()
NoRepeat
实体字段标注:防重复,在新增或修改实体时进行字段的重复检查,会提示并记录重复的IdKeyWord
实体字段标注:关键字,用于关键字搜索MemberGroup
实体字段标注:对属性进行分组,用于分组更新实体指定字段IgnoreMap
实体字段标注:映射忽略分组,用于实体映射时要忽略指定的字段DkEncode
实体字段标注:数据库实体字段编码/加密,支持三种EncodeType:FuzzySearch
、MyBase64
、AES
AutoWired
服务层类属性标注:可自动注入非私有属性DbConnectionName
DbContext类标注:以获取对应的数据库连接 DynamicService
接口层接口标:基于EMIT的动态编译,自动实现接口的本地实现或远程代理实现DisableRoute
接口标注:禁止外部请求接口(仅限内部服务使用),不暴露到网络中DistributedCache
接口标注:分布式接口缓存,减少数据库查询,建议应用于Query中PreventDuplicateSubmit
接口标注:分布式防重复提交,建议应用于UseCase中Signature
接口标注:对接口要求签名。可以使用SignatureHelper.SetSignature
来为继承自ISignatureDto
的类设置签名BasicAuth
接口标注:简易身份认证系统,需要标注AllowAnonymous
后使用,用于swagger文档等ITenant
IDepartment
IRowVersion
IHistoryRecord
IHasExtraProperties
IEntityValidate
ICreateAggregateRoot
,IUpdateAggregateRoot
,IFullAuditedAggregateRoot
[AutoWired]
Lazy<T>
RSA非对称加密(DES加密(密码+验证码))
传输ASP.NET 和 Web 开发
,单个组件-.NET 6.0运行时
项目采用DDD(领域驱动)理念设计,通过新建/复制项目模板
,得到以下项目结构:
*.Application
,业务层EntityFrameworkCore
.Shared
用途:本领域的接口实现和服务
*.Domain
,领域层Dakua.Common.EntityFrameworkCore
.Shared
用途:本领域的Entity及Entity的验证规则,存储设施仓储接口
*.EntityFrameWorkCore
,基础设施层Domain
用途:定义本领域的DbContext及仓储接口实现
*.HttpApi
,API层.Application
Dakua.Common.NacosExtensions
【可选】
用途:本领域的HttpApi,请注意参考Startup
和Program
的配置
*.Shared
,其它领域分享层(DTO及接口)Dakua.Common.DynamicService
Dakua.Common.CoreApp
用途:本领域的DTO、接口、枚举、常量定义
框架在启动时,会按顺序执行以下操作:
IAcModule
,并初始化模块ISingletonDependency
,IScopedDependency
,ITransientDependency
[Table("kd_admin_user")]
来映射AdminUser 或 [Column("kd_index_no")]
来映射IndexNoIDbContext
IDbContextRead
[NotMapped]
,注意:NotMapped后无法Include
服务之间的调用,引入相应领域的Share
项目即可实现基于Http
的微服务调用接口实现,引入相应领域的Application
项目即可直接接口实现
服务之间使用HTTP请求接口,接口基于AccessToken授权验证,可以应用于内网或外网访问。
对外restfull接口调用
Bearer {Token}
或Query的URL中AccessToken={Token}
Signature
),详情参考相关不同语言的签名方法后端签名方法,使用SignatureHelper.SetSignature
来为继承自ISignatureDto
的类设置签名
后端JAVA签名方法
/**
* 签名生成算法
* @@param HashMap<string,string> params 请求参数集,所有参数必须已转换为字符串类型
* @@param String secret 签名密钥
* @@return 签名
* @@throws IOException
*/
public static String getSignature(HashMap<string,string>
params, String secret) throws IOException
{
// 先将参数以其参数名的字典序升序进行排序
Map<string, string>
sortedParams = new TreeMap<string, string>
(params);
Set<entry<String, String>> entrys = sortedParams.entrySet();
// 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
StringBuilder basestring = new StringBuilder();
for (Entry<String, String> param : entrys) {
basestring.append(param.getKey()).append("=").append(param.getValue());
}
basestring.Append(",").Append(secret);
// 使用MD5对待签名串求签
byte[] bytes = null;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
bytes = md5.digest(basestring.toString().getBytes("UTF-8"));
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}
// 将MD5输出的二进制结果转换为小写的十六进制
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex);
}
return sign.toString().toUpperCase();
}
前端JS签名方法
var getSignature = function (myParam, secret) {
// 对参数名进行字典排序
var array = new Array();
for (var key in myParam) {
array.push(key);
}
array.sort();
// 拼接有序的参数名-值串
var paramArray = new Array();
for (var index in array) {
var key = array[index];
paramArray.push(key + "=" + myParam[key]);
}
paramArray.push("," + secret);
// MD5签名,并转换成大写
var shaSource = paramArray.join("");
var sign = hex_md5(shaSource).toUpperCase();
return sign;
}
//调用方法:
// 定义申请获得的secret(signKey)
var secret = "pethome";
// 创建参数表
var param = {};
param["strWhere"] = "1=1";
param["pageSize"] = "20";
param["page"] = "1";
param["orderFld"] = "id";
param["isDesc"] = "true";
param["timestamp"] = (new Date().getTime() / 1000).toFixed(0);//Unix时间戳
var Signature = getSignature(param, secret);
// 字符串连接示例
// isDesc=trueorderFld=idpage=1pageSize=20strWhere=1=1timestamp=1450528907,mysecret
// 签名示例
// 41C3DEBCB735A4440DBFE2BF6E32BC61
开发技术栈
改写接口功能
namespace Dakua.Admin.Application;
/// <summary>
/// 系统后台工具查询改写
/// </summary>
public class SystemToolQueryRewrite : SystemToolQuery
{
/**
* 需要重新注入 ISystemToolQuery
* services.AddScoped<ISystemToolQuery, SystemToolQueryRewrite>();
*/
public SystemToolQueryRewrite(IServiceProvider sp) : base(sp) { }
public new async ValueTask<ResultEntity<OutUploadFileDto>> FileUpLoad(IFormFile file)
{
return await base.FileUpLoad(file);
}
}
数据库操作
通过EF操作
//注入DbContext,推荐使用池化方式,代码参考模板...
//------------------------
//Repositories
services.AddDefaultRepositories<AdminDbContext>(); //6.2.1版本后不需要此操作
//属性注入 + 延迟注入
protected IEfRepository<Temp_FullAuditTable> TempFullAuditTableRepository => LazyTempFullAuditTableRepository.Value;
[AutoWired] protected Lazy<IEfRepository<Temp_FullAuditTable>> LazyTempFullAuditTableRepository { get; set; }
public async ValueTask<ResultEntity<bool>> CreateEFAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await TempFullAuditTableRepository.AddAsync(entity, true);
return isOk.GetResultEntity();
}
public async ValueTask<ResultEntity<bool>> CreateEFUnitOfWorkAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await TempFullAuditTableRepository.AddAsync(entity);
return isOk.GetResultEntity();
}
通过SqlSugar操作
//注入DbContext,推荐使用池化方式,代码参考模板...
//------------------------
//SqlSugar
services.AddDefaultSqlSugarRepositories<AdminDbContext>(AdminDbContext.ConnectionStringName, SqlSugar.DbType.SqlServer); //6.2.1版本后不需要此操作
//属性注入 + 延迟注入
protected ISqlSugarRepository<AdminDbContext> SqlSugarTempFullAuditTableRepository => LazySqlSugarTempFullAuditTableRepository.Value;
[AutoWired] protected Lazy<ISqlSugarRepository<AdminDbContext>> LazySqlSugarTempFullAuditTableRepository { get; set; }
public async ValueTask<ResultEntity<bool>> CreateSqlSugarAsync(CreateTempFullAuditTableDto input)
{
var entity = input.MapTo<Temp_FullAuditTable>();
var isOk = await SqlSugarTempFullAuditTableRepository.AddAsync(entity);
return isOk.GetResultEntity();
}
对象映射,默认为深度复制(如:DTO与实体Entity,支持class到Dictionary映射)
src.MapTo<Destination>(); //直接映射
src.MapTo(entity); //覆盖映射
src.MapTo(entity, new List<string>() { "Name" });//覆盖时忽略指定列
使用带时间顺序的GUID生成器
IGuidGenerate guidGenerator //注入IGuidGenerate 即可
获取HttpContextAccessor
或HttpContext
IHttpContextAccessor httpContextAccessor //注入IHttpContextAccessor
httpContextAccessor.HttpContext //获取HttpContext
通过HTTP访问外部服务(如:上报数据)
IDakuaHttpClient dakuaHttpClient //注入IDakuaHttpClient
dakuaHttpClient.GetClient() //获取HttpClient
dakuaHttpClient.PostData<T>(...) //post json
dakuaHttpClient.GetData<T>(...) //get url
dakuaHttpClient.PostFormData<T>(...) //post form
dakuaHttpClient.RequestData<T>(...) //底层方法
DTO输出时的加密处理,使用[JsonConverter(typeof(JsonEncryptConverter))]
标注
/// <summary>
/// 输出加密用户信息
/// </summary>
[JsonConverter(typeof(JsonEncryptConverter))]
public string UserInfo { get; set; }
DTO输出时的隐藏机密信息,使用[JsonConverter(typeof(JsonHideConverter)...]
标注
/// <summary>
/// 输出从第3位开始隐藏4位的手机号
/// </summary>
[JsonConverter(typeof(JsonHideConverter), 3, 4)]
public string Phone { get; set; }
DTO输出时的枚举翻译,使用枚举字段.GetDescription()
即可
/// <summary>
/// 字典类型
/// </summary>
public DictionaryTypeEnum DictionaryType { get; set; } = 0;
/// <summary>
/// 字典类型文本
/// </summary>
public string DictionaryTypeTxt => DictionaryType.GetDescription();//枚举转换
DTO输出时的字典翻译,使用字典Code字段.GetDictNameByCode(字典编码)
即可
/// <summary>
/// Code
/// </summary>
[MaxLength(20)]
[NoRepeat]
public string Code { get; set; }
/// <summary>
/// Gets the code text.
/// </summary>
public string CodeTxt => Code.GetDictNameByCode("DictCode");//字典转换
DTO输出时不输出指定的属性(输出null),只需要在MapTo时指定忽略的列属性即可,或者标注[JsonIgnore]
entity.MapTo<TOutputDto>(q => new { q.Id });//不转换Id
//批量转换
//dataList.MapTo<List<TOutputDto>>();
dataList.Select(q => q.MapTo<TOutputDto>(w => w.Id)).ToList();
实体属性JSON内容转CLASS功能
IHasExtraProperties
,必须有一个[Key]
注解/// <summary>
/// 测试Json子类
/// </summary>
public class AdminAreasExtJson: IHasExtraProperties
{
[Key]
public bool IsOK { get; set; } = true;
public string Tips { get; set; } = "";
}
public AdminAreasExtJson ExtJson { get; set; }
内置控制器/动态代理接口注解功能
[PreventDuplicateSubmit] //防止一段时间内的重复提交
[DynamicService(ConstAuthPermission.ServiceName)] //指定动态代理服务
[ApiExplorer("AdminManage")] //指定Swagger分组
[UnitOfWork] //指定使用工作单元包裹方法
public interface ITempFullAuditTableUseCase : IDynamicService
{
/// <summary>
/// 新增
/// </summary>
/// <param name="input">dto</param>
/// <returns></returns>
[HttpPost]
[Route("/api/biz/TempFullAuditTable")]
[ApiAuth(ConstAuthPermission.BaseAdmin, RoleAction.Create)]
ValueTask<ResultEntity<bool>> CreateAsync(CreateTempFullAuditTableDto input);
}
内置实体注解实体功能
/// <summary>
/// 编码
/// </summary>
[MaxLength(50)] //实体验证配置
[Required] //不能为null
[KeyWord] //关键字搜索
[NoRepeat("编码")] //不允许重复,提示名称为‘编码’
[MemberGroup("Update|ABC")]//指定可以通过update和ABC来进行分组检索字段
[IgnoreMap("Out|MiniOut")]//指定可以通过Out和MiniOut来忽略MapTo时的字段映射
public string DictionaryCode { get; set; }
内置实体可继承接口功能
BaseAggregateRoot
、BaseEntity
、BaseAggregateRootCross
、BaseEntityCross
,默认有CreateBy
、CreateTime
、SetNewId()
、GetMemberListByMemberGroup()
、GetMemberNameListByMemberGroup()
ICreateAggregateRoot
,默认有CreateBy
、CreateTime
IUpdateAggregateRoot
,默认有ICreateAggregateRoot
所有属性加上UpdateBy
、UpdateTime
IFullAuditedAggregateRoot
,默认有IUpdateAggregateRoot
所有属性加上DeleteBy
、DeleteTime
、IsDelete
IDepartment
用户及部门数据权限支持,默认有UserId
、DepartId
ITenant
多租户支持,默认有TenantId
IRowVersion
更新时版本号检查(乐观锁),默认有RowVersion
IHasExtraProperties
扩展列接口标识(将JSON转成Class,限EF),先将子类继承于本接口(子类必须有一个属性为[Key]),再在父类中使用子类为属性类型即可IHistoryRecord
自动历史记录,将在历史记录表中创建数据,要在前端查看则需要返回附加数据自动注入
[AutoWired]
即可,public
、internal
、protected
均可注入,建议使用internal
延迟注入
[AutoWired] protected Lazy<SystemPermissionQuery> LazySystemPermissionQuery { get; set; }
protected SystemPermissionQuery SystemPermissionQuery => LazySystemPermissionQuery.Value;
[AutoWired] protected Lazy<SystemPermissionUseCase> LazySystemPermissionUseCase { get; set; }
private SystemPermissionUseCase SystemPermissionUseCase => LazySystemPermissionUseCase.Value;
数据库内容加密(仅适用于string类型,目前仅自动处理EF的新增/修改/查询),EncodeType支持三种:FuzzySearch
自动模糊搜索,MyBase64
乱序Base64,AES
对称加密
[DkEncode]
即可 [DkEncode(encodeType)]
public string Name { get; set; }
缓存使用
//分布式缓存(Redis)
protected IRedisHelper RedisHelper
//内存缓存
protected IMemoryCache MemoryCache
分布式消息队列,需要引用Nuget包Dakua.Common.CAP
,然后注入ICAPHelper capHelper
后使用
//发送消息队列
await _capHelper.RecordAndPublishMQ<AdminDbContext>(sendDto.MqEvent, sendDto.DataJson, sendDto.BizNo, (dataId) => new Dictionary<string, string>()
{
//MQRecordId
["SendMqId"] = dataId.ToString(),
//失败重试次数(通过定时服务)
["FailedRetryCount"] = "2",
//失败重试间隔(分)
["FailedRetryInterval"] = "5",
});
/// <summary>
/// 分布式消息处理服务
/// </summary>
public class MyCapService : ICapSubscribe
{
private readonly ICAPHelper _capHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MyCapService"/> class.
/// </summary>
/// <param name="capHelper">The cap helper.</param>
public MyCapService(ICAPHelper capHelper)
{
_capHelper = capHelper;
}
/// <summary>
/// 接收指定为[mqEvent]的消息,并处理
/// </summary>
/// <param name="eventData">The event data.</param>
[CapSubscribe("mqEvent")]
public async void MyEventTodo(CapEventData<object> eventData)
{
await _capHelper.UpdateAndRunMq<AdminDbContext>(eventData, async (record) =>
{
//业务处理
var isOk = await todoBiz(record);
return new CapActionResult() { IsOK = isOk, Msg = isOk ? "执行成功" : "执行失败" };
});
}
}
JSON序列化,使用Newtonsoft.Json
,需要:using Dakua.Common;
class.ToJsonString()
"jsonString".ToClassByJson<class>();
应用程序级消息,参考使用MediatR
工作单元提交,基于注解[UnitOfWork]
动态代理的接口
和Controller
类及方法上[UnitOfWork]
,可以方法上标注[NoUnitOfWork]
取消本方法使用工作单元提交认证授权与权限菜单
[AllowAnonymous]
[ApiAuth(ConstAuthPermission.BaseAdmin, RoleAction.Create)]
,分别为权限字符串及权限动作。增加权限菜单
,然后通过账号及角色授权到权限
。实体校验
System.ComponentModel.DataAnnotations
/// <summary>
/// 排序号
/// </summary>
[Required]
public int IndexNo { get; set; } = 0;
/// <summary>
/// 扩展信息
/// </summary>
[MaxLength(8000)]
public string ExtJson { get; set; }
IEntityValidate<EntityClassName>
//实体验证
public void ValidateAdd(ValidateContext<EntityClassName> validateContext)
{
ValidateCommon(validateContext, true);
}
public void ValidateUpdate(ValidateContext<EntityClassName> validateContext)
{
ValidateCommon(validateContext);
}
private static void ValidateCommon(ValidateContext<EntityClassName> validateContext, bool isAdd = false)
{
//请求实体不能为null,否则直接中断校验
validateContext.MustNotNull().IfFailThenExit();
//必须不为0
//validateContext.RuleFor(i => i.Level).MustNotEqualTo(0);
//不能为空且长度在1-4之间
//validateContext.RuleFor(i => i.FullName).MustNotNullOrEmptyOrWhiteSpace().MustLengthInRange(2, 20);
}
DTO校验
System.ComponentModel.DataAnnotations
/// <summary>
/// 排序号
/// </summary>
[Required]
public int IndexNo { get; set; } = 0;
/// <summary>
/// 扩展信息
/// </summary>
[MaxLength(8000)]
public string ExtJson { get; set; }
IDtoValidate<DtoClassName>
public void Validate(ValidateContext<DtoClassName> validateContext)
{
//请求实体不能为null,否则直接中断校验
validateContext.MustNotNull().IfFailThenExit();
//必须不为string
//validateContext.RuleFor(i => i.Province).MustNotEqualTo("string");
}
自动CRUD泛型服务,需要继承自AutoUseCaseApplicationService
和AutoQueryApplicationService
、AutoFullQueryApplicationService
public virtual async ValueTask<ResultEntity<bool>> CreateAsync(TCreateInput input);
public virtual async ValueTask<ResultEntity<bool>> UpdateAsync(TUpdateInput input);
public virtual async ValueTask<ResultEntity<bool>> DeleteAsync(Guid id);
public virtual async ValueTask<ResultEntity<bool>> DeleteBatchAsync(string ids);
/// <summary>
/// 获取单个
/// </summary>
/// <param name="id">Id</param>
/// <returns></returns>
public async ValueTask<ResultEntity<TGetOutputDto>> GetAsync(Guid id);
/// <summary>
/// 获取翻页列表
/// </summary>
/// <param name="input">翻页数据</param>
/// <returns></returns>
public async ValueTask<ResultEntity<List<TGetListOutputDto>>> GetListAsync(TGetListInput input);
/// <summary>
/// 回收站(AutoFullQueryApplicationService 专有)
/// </summary>
/// <param name="input">翻页数据</param>
/// <returns></returns>
public async ValueTask<ResultEntity<List<TGetListOutputDto>>> GetListRecycleAsync(TGetListInput input);
本地化及多语言支持,依赖注入IStringLocalizer
或IStringLocalizer<Class>
即可,Class
最好标注LocalizationResourceName
来指定查找路径
//service中配置
services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); //在/Resources目录中配置语言
//Configure中配置支持的多语言版本(建议从配置文件读取)
app.UseRequestLocalization("en-US", "fr-FR");
//MVC的view使用需要配置
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(RegisterViewModel));
});
/***
* 使用:
* 1、在Resources目录中给出对应的语言,如en.json的内容为{ "你好":"Hello","操作异常,请稍候再试": "Abnormal operation, please try again later",... }
* 2、注入IStringLocalizer或者继承自BaseApplicationService后使用L["你好"]
***/
基础服务继承可使用的属性,需要继承自BaseApplicationService
或他的子类
//多语言支持,如:L["你好"]
protected IStringLocalizer L;
//时间顺序GUID工具类
protected IGuidGenerate GuidGenerator;
//当前认证账号
protected ICurrentIdentifyUser CurrentUser;
//HttpContextAccessor
protected IHttpContextAccessor HttpContextAccessor;
//Redis工具类
protected IRedisHelper RedisHelper;
//Excel工具类
protected IExcelHelper ExcelHelper;
//Mediator
protected IMediator Mediator;
//字典转换类
protected ITranslateDict TranslateDict;
//全局配置类
protected IGlobalConfig GlobalConfig;
//用户工具类
protected IAdminUserHelper AdminUserHelper;
//租户工具类
protected ITenantHelper TenantHelper;
//Entity工具类
protected IEntityHelper EntityHelper;
//当前用户Id
protected Guid CurrentUserId => CurrentUser.Id;
//当前用户信息
protected RedisAdminUserDto CurrentAdminUser;
//当前用户是否超级管理员
protected bool IsSuperAdmin;
//当前租户Id
protected Guid CurrentTenantId;
//当前租户信息
protected AdminTenantDto CurrentTenant;
//当前用户权限部门(不包括子部门,仅包括选择部门)
protected List<AdminUserDepartment> CurrentUserDepartments;
//当前用户登录选择的权限部门(包括子部门继承)
protected List<AdminUserDepartment> CurrentUserAllDepartments;
//执行事务提交,包含仓储未保存的修改
protected async ValueTask<int> CommitRepositoryAsync(params IEFCoreRepository[] repositories);
//执行事务提交,包含仓储未保存的修改,同时执行SQL
protected async ValueTask<int> CommitRepositoryAsync(List<CommitSQLDto> commitSqlList, params IEFCoreRepository[] repositories);
// 雪花Id
protected long NewSnowFlakeID();
// 时间顺序的GUID
protected Guid NewTimeGuid();
/// <summary>
/// 获取模糊匹配查询表达式(根据查询DTO)
/// </summary>
/// <typeparam name="TDto">The type of the dto.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="dto">查询dto</param>
/// <param name="exp">现有需要附加的表达式</param>
/// <param name="noExp">不处理的字段表达式,如:q => new { q.Name }</param>
/// <param name="isAnd">表达式之间是否使用And连接,为false时将使用Or连接</param>
/// <returns></returns>
public static Expression<Func<TEntity, bool>> GetExpressionByQueryDto<TDto, TEntity>(TDto dto
, Expression<Func<TEntity, bool>> exp = null
, Expression<Func<TDto, object>> noExp = null
, bool isAnd = true)
where TEntity : class, new()
where TDto : class, new()
=> GetExpressionEqualOrContainsByQueryDto(dto, exp, noExp, isAnd);
/// <summary>
/// 获取精确匹配查询表达式(根据查询DTO)
/// </summary>
/// <typeparam name="TDto">The type of the dto.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="dto">查询dto</param>
/// <param name="exp">现有需要附加的表达式</param>
/// <param name="noExp">不处理的字段表达式,如:q => new { q.Name }</param>
/// <param name="isAnd">表达式之间是否使用And连接,为false时将使用Or连接</param>
/// <returns></returns>
public static Expression<Func<TEntity, bool>> GetExpressionEqualByQueryDto<TDto, TEntity>(TDto dto
, Expression<Func<TEntity, bool>> exp = null
, Expression<Func<TDto, object>> noExp = null
, bool isAnd = true)
where TEntity : class, new()
where TDto : class, new()
=> GetExpressionEqualOrContainsByQueryDto(dto, exp, noExp, isAnd, false);
常见问题:
last update 2023-11-10, by www.dakua.net