dotnetcore / dotnetcore/osharp

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

Open
#120 1 comment 11 reactions 0 assignees View on GitHub
Useful :100:
Dominant language
C#
Stars
2.8k
Forks
763
PR merge metrics
No merged PRs in 30d

Description

  使用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的类型,动态添加Controller。由于1,2两点已经实现,到时候会发现,只写一个Entity,没有Input,Output,Service, Swagger中已经有这个Entity默认的CRUD Api了。对敏捷开发,手动临时添加Entity(非代码生成)时有点实用。
```C#
AddMvc()
.ConfigureApplicationPartManager(apm => apm.FeatureProviders.Add(new XXXControllerFeatureProvider(services)))
```

## 4 Swagger引入xml注释的问题
```C#
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动态创建类型;2 调用AutoMapper动态注册已有类型和动态类型的映射;3 把AutoMapper中的ProjectTo调用方式变成ProjectTo(typeof(dynamicType);

## 2 多租户
 站点的数据可能是用户独享,也可能是某一组人(如公司)独享的。
  参考:将权限服务独立开后,就可以在自定义的权限服务中写自己的过滤逻辑了:if(entity is ITanented as tanented){ ...Where(it=>it.TanenetId == xxx) }

# 七 附录
## 1 ServiceBase接口的定义(起中转作用)
```C#
{
IQueryable Entities { get; }

// T可以是TEntity,也可以是TOutput;dto可以是Entity, 也可以InputDto。Get, Update同。
Task> Create(params object[] dtos);

Task Delete(params TKey[] ids);
Task> Update(params object[] dtos);
T Get(TKey id) where T : class;
PageData Read(PageRequest request, params string[] properties);
IEnumerable ReadAll(params string[] properties);
}
```

## 2 减少TypeFinder的提议示例
```C#
public class ThModuleContainer
{
public static List ThModules;
public static List 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()
.Select(it => it.Assembly)
.Where(FilterAssemblies)
.ToList();

ThModules = FindByBase()
.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 FindTypes(Func predicate)
{
return Assemblies.SelectMany(it => it.ExportedTypes).Where(predicate);
}

public static IEnumerable FindByBase(bool includeSelf = false)
{
return FindTypes(it => typeof(TBase).IsAssignableFrom(it) && (includeSelf || it != typeof(TBase)));
}

public static IEnumerable FindImplementTypes(params Type[] typesToExclude)
{
return FindTypes(it => typeof(TInterface).IsAssignableFrom(it) && !it.IsInterface && !it.IsAbstract && !typesToExclude.Contains(it));
}

public static IEnumerable FindByAttribute() where TAttribute:Attribute
{
return FindTypes(it => it.GetCustomAttribute() !=null);
}
}
```

## 3 UnitOfWork的改进示例
```C#
public class UnitOfWorkAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// 避免使用ServiceFilter中转
var unitOfWork =context.HttpContext.RequestServices.GetService();
unitOfWork.Enable();
}

public void OnActionExecuted(ActionExecutedContext context)
{
var unitOfWork = context.HttpContext.RequestServices.GetService();

// 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()之间
*/
///
/// 业务单元操作
///
[Dependency]
public class UnitOfWork : IUnitOfWork
{
private readonly IServiceProvider _serviceProvider;
private EntityRegister _entityRegister;
private bool _disposed;

//缓存DBConnection和DbContext的多对多映射关系。同一Connection下(可能)共享事务
private readonly ConcurrentDictionary> _connMap;
private readonly ConcurrentDictionary _transMap;
private List DbContexts=>_connMap.Values.SelectMany(it => it).ToList();

public Stack CallStack = new Stack();

///
/// 初始化一个类型的新实例
///
public UnitOfWork(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_entityRegister = _serviceProvider.GetService();
_connMap = new ConcurrentDictionary>();
_transMap = new ConcurrentDictionary();
}

///
/// 获取 事务是否已提交
///
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});
}
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());

// ...
}
}
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.