osharp icon indicating copy to clipboard operation
osharp copied to clipboard

深入使用OSharp之后的一些反馈建议

Open Champion-Chen opened this issue 5 years ago • 1 comments

  使用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());

              // ...
        }
    }

Champion-Chen avatar Oct 29 '19 06:10 Champion-Chen

看的云里雾里的,但是感觉确实是很厉害的,学习OSharp中。

anyangmaxin avatar Oct 21 '20 01:10 anyangmaxin