osharp
osharp copied to clipboard
深入使用OSharp之后的一些反馈建议
使用O#有一段时间了,非常感谢各位打造了这样一款优秀的开源作品,下面列举一些我个人的建议,如果其中有各位想法有冲突的或言辞不悦的请自行跳过:
一 框架层:
1 Repository
建议让Repository只操作实体,不对DTO进行直接操作。DTO与Entity的转换放在Service中。Service既然可以接受Input,也接受Entity做参数;既可以返回Output也可以返回Entity。让Controller层尽量只调用Service,减少直接调用Repository的需求:这样逻辑层次较清晰一些。
现在业务中,感觉input多做复杂接受参数使用;DTO做复杂输出参数使用。两者并非ViewModel。CodeFirst时代,弱化了Entity的概念,我个人习惯直接用Entity进行逻辑操作。如果对Service返回的对象进行进一步逻辑操作,我们必然需要返回Entity;如果不需要,我们转换成Output进行输出就可以了——所以,这要求Service(根据泛型参数)很好的起到中转作用, 接口定义见附录。
Mapto, OutputTo扩展方法中,可以根据”typeof(TOutput) == source.ElementType"来决定是否不进行映射)
2 对权限操作抽取接口,并声明成服务。
这样,对于不想使用现有权限逻辑的场景,提供了进行自定义的可能。比如,采用AD登录,操作时根据AD中的Group信息进行权限判断。
二 现在写法:
1 TypeFinder有些多,每遍历一种就声明一种对应的Finder。
重构思路(见最后的附,仅供参考):把所有Assembly缓存下来,放在静态类中。该类提供了FindByAttribute, FindBySubClass, FindImplements, Find(Predicate<Type>)等静态方法,全局统一调用。 个人感觉类型查找等并不是太关键的功能,没必要抽成服务以期替换。就算要替换查找逻辑,在调用查找逻辑的类中替换修改条件就可以了,此时要替换的往往是这个用到查找逻辑的类:比如EntityManager。
2 减少DbContextBase中的逻辑。
考虑在Repository中添加方法SaveChanges(Async)。该方法中存放现在属于DbContextBase中的一些逻辑,如数据审计,开启事务,事件通知等移过来。这样处理的好处是强制使用无法继承自DbContextBase的DbContext时也可以享受到这些功能点。(当然,它无法进行Entity注册,应该也不需要,如IDS中要使用的数据库就是指定的,Entity注册写在它的Context中)
3 UnitOfWork和UnitOfWorkImp重构(附录有示例,仅供参考)
具体思路不表。这样可以解决两个问题:1 将UnitOfWork, UnitOfWorkManager, ScopedDictionary合并成了一个类,精简一些;2 事务使用与否,取决于是否加特性。避免了初使用者因为没加特性导致修改不提交的坑。
三 功能增强
1 实体自动注册
之前有个实践中我发现,EfCore根据惯例配置的主键外键等已经够用了(模型比较简单),EntityInfoConfiguration中什么都不写也没问题。 于是留下了很多空xxxEntityConfiguration.cs。 参考:重写了EntityManager,遍历完实体后,会自动创建一个泛型的EntityInfoConfiguration实例,然后添加到dict中。
2 AutoMapper自注册
参考:遍历实体时,如果能查找到 xxx, xxxInputDto, xxxOutputDto这样的命名关系,"根据惯例", 自动建立它们之间的映射关系,不用再单独填写MapTo和MapFrom特性。还要添加一个实体到自身的映射,有些场合的实体比较简单,我会将Entity同时当InputDto和OutputDto使用(手动敲代码的时候)。虽然有点打破分层结构,但比较精简。
3 CrudService基类和CrudController基类
- 减少代码量;
- 每个Entity的API中CRUD采用统一命名,前端不用再针对每个Entity单独写service了。(类似OData)
- (For Fun)可以在程序启动时,自动查找Entity, EntityInputDto??Entity, EntityOutputDto??Entity, 然后创建一个带CrudController<Entity,Key,TInput,TOutput>的类型,动态添加Controller。由于1,2两点已经实现,到时候会发现,只写一个Entity,没有Input,Output,Service, Swagger中已经有这个Entity默认的CRUD Api了。对敏捷开发,手动临时添加Entity(非代码生成)时有点实用。
AddMvc()
.ConfigureApplicationPartManager(apm => apm.FeatureProviders.Add(new XXXControllerFeatureProvider(services)))
4 Swagger引入xml注释的问题
var xmlFile = $"{Assembly.GetEntryAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath)) options.IncludeXmlComments(xmlPath);
四 内核实现
1 PageRequest相关。
如果使用DynamicLinq做条件解析工具,那么多们就只需要将PageRequest中的Rule翻译成对应的字符串表达形式和条件参数就行了。Sort条件变成字符串更方便。记得SortCondionts转换成Sort表达式的代码还有些复杂。相比现在自己写转换条件要轻松一些,而且可以轻松实现In和NotIn运算符,也便于以后扩展
五 结构调整
1 将EntityConfiguration和Entity放在一起(尤其是在有了Entity自注册以后)
未来如果实现了模型的验证集成(如使用FluentValidation,把IEntityValidator也放一起好了。同一个Entity的东西,放一起显得紧凑一些。如果有运维需求,如单独替换Entity的Service.dll, 或BizLogic.dll,项目得够大。
2 可以考虑把一些非功能性的内容(如common, security, system,identity等)移到进阶功能可选包中
-> 初级上手人员,或不使用o#内置权限,认证还有前端站点的人 —— 只安装必要的dll: 学习和使用利用O#生成或编写CRUD服务给前端调用 感觉就是有不少人把O#当Asp.net入门教材使用的;这一步的sample程序,只有一个本地数据库,没有前端界面。想测试接口到swagger中去点。后端前端都不懂的情况,直接介入了前端编译,认证等东东,入口的坡有点陡了,很容易被“辣鸡~”。正常情况,建议他们会使用O#提供的基类快速开发Service后,阅读一下实现原理。再在此基础上研究其它逻辑:如权限,审计等是怎么介入的,这个时候就需要下面的东西了。 -> 想研究或使用这些权限和认证的 —— 单独引入project后,再感受框架是怎么认证,怎么进行权限控制的。
个人感觉,框架要周全,库要够精够深。比如json.net(现在的Newtonsoft.json),automapper。库成长为框架的过程中,必然是自身得到了认可,后面不断扩展的结果,如IdentityServer。 O#单独使用时,它可以是一个库(还有代码生成),可以帮你很快生成CRUD的api,让前端有真实模拟数据。(而且Entity配置,Service注册,DTO映射,模块化注册给Startup瘦身,现成的包含DTO转换逻辑的Repository, 方便的事务,不论对新手门,还是小型开发者,还是相当有便利的);同时它有足够的接口,配合其生态上的东西使用时,它便是一个框架了。现在通过正常操作产生的示例项目,有点【这就是一个框架,无法当库使用,要用就用全套】的感觉,失去了做为库的灵活性。
六 展望功能
1 动态列查询
前端下拉表中,选择哪些人,哪些单据的时候,我们会从后端查询Entity的全部数据。如果此时把Entity的全部属性返回,总觉得有点不优雅;如果说单独开一个方法,如QueryXXXDicts,然后把数据结构进行转换,又有点累赘。而且前端需要的列可能会变动(比如级联查询会用到一些外键),此时最好可以基于动态返回指定属性。 参考:1 DynamicLinq可以根据Dictionary<name, type>动态创建类型;2 调用AutoMapper动态注册已有类型和动态类型的映射;3 把AutoMapper中的ProjectTo<TDynamic>调用方式变成ProjectTo(typeof(dynamicType);
2 多租户
站点的数据可能是用户独享,也可能是某一组人(如公司)独享的。 参考:将权限服务独立开后,就可以在自定义的权限服务中写自己的过滤逻辑了:if(entity is ITanented as tanented){ ...Where(it=>it.TanenetId == xxx) }
七 附录
1 ServiceBase接口的定义(起中转作用)
{
IQueryable<TEntity> Entities { get; }
// T可以是TEntity,也可以是TOutput;dto可以是Entity, 也可以InputDto。Get, Update同。
Task<IEnumerable<T>> Create<T>(params object[] dtos);
Task<int> Delete(params TKey[] ids);
Task<IEnumerable<T>> Update<T>(params object[] dtos);
T Get<T>(TKey id) where T : class;
PageData Read<T>(PageRequest request, params string[] properties);
IEnumerable ReadAll<T>(params string[] properties);
}
2 减少TypeFinder的提议示例
public class ThModuleContainer
{
public static List<ThModule> ThModules;
public static List<Assembly> Assemblies;
public static void Init(IServiceCollection services)
{
// 需添加MVC,或MVCCore,利用ApplicationPartManager帮我们遍历加载程序集。不用自己写代码。
services.AddMvc();
var amp = (ApplicationPartManager)services.FirstOrDefault(it => it.ServiceType == typeof(ApplicationPartManager))
.ImplementationInstance;
Assemblies = amp.ApplicationParts.Where(it => it is AssemblyPart)
.Cast<AssemblyPart>()
.Select(it => it.Assembly)
.Where(FilterAssemblies)
.ToList();
ThModules = FindByBase<ThModule>()
.Select(it => (ThModule)Activator.CreateInstance(it))
.OrderBy(it => it.Level)
.ToList();
//重排程序优先级,保证类型的初始化也按模块的优先级来。
Assemblies = Assemblies
.OrderBy(it => ThModules.FirstOrDefault(it2 => it2.GetType().Assembly == it)?.Level ?? 20)
.ToList();
}
private static bool FilterAssemblies(Assembly assembly)
{
return !assembly.GetName().Name.StartsWith("Microsoft");
}
public static IEnumerable<Type> FindTypes(Func<Type, bool> predicate)
{
return Assemblies.SelectMany(it => it.ExportedTypes).Where(predicate);
}
public static IEnumerable<Type> FindByBase<TBase>(bool includeSelf = false)
{
return FindTypes(it => typeof(TBase).IsAssignableFrom(it) && (includeSelf || it != typeof(TBase)));
}
public static IEnumerable<Type> FindImplementTypes<TInterface>(params Type[] typesToExclude)
{
return FindTypes(it => typeof(TInterface).IsAssignableFrom(it) && !it.IsInterface && !it.IsAbstract && !typesToExclude.Contains(it));
}
public static IEnumerable<Type> FindByAttribute<TAttribute>() where TAttribute:Attribute
{
return FindTypes(it => it.GetCustomAttribute<TAttribute>() !=null);
}
}
3 UnitOfWork的改进示例
public class UnitOfWorkAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// 避免使用ServiceFilter中转
var unitOfWork =context.HttpContext.RequestServices.GetService<IUnitOfWork>();
unitOfWork.Enable();
}
public void OnActionExecuted(ActionExecutedContext context)
{
var unitOfWork = context.HttpContext.RequestServices.GetService<IUnitOfWork>();
// TO O# Devs: 我们的项目不允许将错误包含在result中,有错误就throw直接中断;否则不视为有错误。中间件会根据错误的类型编码转换相应的httpstatuscode, 前端会根据不同的错误类型决定要 隐藏真实消息(如服务器挂了, 参数传错了) ,还是直接将消息显示给用户看(如上传的记录有错误)
if (context.Exception == null)
{
unitOfWork.Commit();
}
// 根据官方说法,unitOfWork Dispose时会自动触发事务回滚。不需要单独写出来。
// else
// {
// unitOfWork.Rollback();
// }
}
/**
* 改进点:
* 1 目前的情况,如果Controller中,两个方法都标记了UnitOfWork,而且其中一个方法调用了另一个,似乎被调用的提交了事务后,调用方再次就提交不了了,因为HadCommitted字段已设置为True,而且两个方法中共享同一UnitOfWork实例。
* 为了避免这种情况,我将Enabled设置成了栈。如果栈里有大于1个值,前面Commit时就只出栈但不进行真正的Commit,直到最后一个值 ————这样等于变向模拟了环境事务(目前版本中,Mysql的适配器并不支持环境事务);
*
* 2 目前(虽然不使用但)根据connection对dbContexts进行了分组。因为不同的Connection间不能共享事务(UseTransaction)
*
* 3 如果要在Service中使用事务,而非Controller中。需要自己注入一个IUnitOfWork。然后将使用
* 事务的代码包裹在 uw.Enable()与uw.Commit()之间
*/
/// <summary>
/// 业务单元操作
/// </summary>
[Dependency]
public class UnitOfWork : IUnitOfWork
{
private readonly IServiceProvider _serviceProvider;
private EntityRegister _entityRegister;
private bool _disposed;
//缓存DBConnection和DbContext的多对多映射关系。同一Connection下(可能)共享事务
private readonly ConcurrentDictionary<DbConnection,List<DbContext>> _connMap;
private readonly ConcurrentDictionary<DbConnection,DbTransaction> _transMap;
private List<DbContext> DbContexts=>_connMap.Values.SelectMany(it => it).ToList();
public Stack<bool> CallStack = new Stack<bool>();
/// <summary>
/// 初始化一个<see cref="UnitOfWork"/>类型的新实例
/// </summary>
public UnitOfWork(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_entityRegister = _serviceProvider.GetService<EntityRegister>();
_connMap = new ConcurrentDictionary<DbConnection, List<DbContext>>();
_transMap = new ConcurrentDictionary<DbConnection, DbTransaction>();
}
/// <summary>
/// 获取 事务是否已提交
/// </summary>
public bool HasCommitted { get; private set; }
public void Enable()
{
CallStack.Push(true);
}
public bool Enabled => CallStack.Count > 0;
public bool ShouldCommit()
{
var flag = CallStack.Count == 1;
if (CallStack.Any()) CallStack.Pop();
return flag;
}
public DbContext GetDbContext(Type entityType)
{
// entity -> which db dbContext -> which connection?
var dbContextType = _entityRegister.FindDbContext(entityType);
var dbContext = DbContexts.FirstOrDefault(m => m.GetType() == dbContextType);
if (dbContext != null)
{
return dbContext;
}
dbContext =(DbContext)_serviceProvider.GetService(dbContextType);
new AppException($"数据上下文“{dbContext.GetType().FullName}”的数据库不存在,请通过 Migration 功能进行数据迁移创建数据库。").ThrowIf(!dbContext.ExistsRelationalDatabase());
var connection = dbContext.Database.GetDbConnection();
if (_connMap.ContainsKey(connection))
{
_connMap[connection].Add(dbContext);
}
else
{
_connMap.TryAdd(connection,new List<DbContext>{dbContext});
}
return dbContext;
}
// NOTE: to O# devs: Enabled的存在,让真正开启事务有了先决条件。
public virtual void BeginOrUseTransaction(DbContext dbContext)
{
if (_connMap.IsEmpty || !Enabled)
{
return;
}
foreach (var connection in _connMap.Keys)
{
if (!_connMap[connection].Contains(dbContext)) continue;
if (connection.State!=ConnectionState.Open)connection.Open();
if (!_transMap.TryGetValue(connection, out DbTransaction transaction))
{
transaction = connection.BeginTransaction(); //开启事务
_transMap.TryAdd(connection, transaction);
}
if (dbContext.Database.CurrentTransaction != null &&
dbContext.Database.CurrentTransaction.GetDbTransaction() == transaction)
{
continue;
}
if (dbContext.IsRelationalTransaction())
{
dbContext.Database.UseTransaction(transaction);
}
else
{
dbContext.Database.BeginTransaction(); //非关系型数据库单独开启事务
}
}
HasCommitted = false;
}
public virtual async Task BeginOrUseTransactionAsync(DbContext dbContext, CancellationToken cancellationToken = default(CancellationToken))
{
//....
}
// to O# devs: 【ShouldCommit()】
public virtual void Commit()
{
if (HasCommitted || _connMap.IsEmpty || _transMap.IsEmpty||!ShouldCommit())
{
return;
}
_transMap.Values.ToList().ForEach(it => it.Commit());
HasCommitted = true;
}
public virtual void Rollback()
{
// ...
_transMap.Values.ToList().ForEach(it => it.Rollback());
// ...
}
public void Dispose()
{
// ...
_transMap.Values.ToList().ForEach(it => it.Dispose());
// ...
}
}
看的云里雾里的,但是感觉确实是很厉害的,学习OSharp中。