Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Champion-Chen opened this issue Oct 29, 2019 · 1 comment
Open

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

Champion-Chen opened this issue Oct 29, 2019 · 1 comment
Labels
Useful 💯 对于解决问题有帮助

Comments

@Champion-Chen
Copy link

Champion-Chen commented Oct 29, 2019

  使用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)等静态方法,全局统一调用。
 个人感觉类型查找等并不是太关键的功能,没必要抽成服务以期替换。就算要替换查找逻辑,在调用查找逻辑的类中替换修改条件就可以了,此时要替换的往往是这个用到查找逻辑的类:比如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调用方式变成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());

              // ...
        }
    }
@anyangmaxin
Copy link

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

@gmf520 gmf520 added the Useful 💯 对于解决问题有帮助 label Mar 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Useful 💯 对于解决问题有帮助
Projects
None yet
Development

No branches or pull requests

3 participants