OSharp是什么?
OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现。与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现。依赖注入、ORM、对象映射、日志、缓存等等功能,都只定义了一套最基础最通用的抽象封装,提供了一套统一的API、约定与规则,并定义了部分执行流程,主要是让项目在一定的规范下进行开发。所有的功能实现端,都是通过现有的成熟的第三方组件来实现的,除了EntityFramework之外,所有的第三方实现都可以轻松的替换成另一种第三方实现,OSharp框架正是要起隔离作用,保证这种变更不会对业务代码造成影响,使用统一的API来进行业务实现,解除与第三方实现的耦合,保持业务代码的规范与稳定。
本文已同步到系列目录:OSharp快速开发框架解说系列
在《【开源】OSharp框架解说系列(6.1):日志系统设计》中,我们已经设计并实现了一个可扩展的日志系统,只要定义好输出端的Adapter,就可以以任意形式输出日志信息。
在系统开发中,有些日志记录需求是常规需要的,比如操作日志,数据变更日志,系统异常日志等,我们希望把这些常规需求都集成到OSharp框架当中。有了内置的支持,在做开发的时候,只需要很简单的配置,就可以实现相关需求。
关于三类日志,这里先简要描述一下:
在OSharp框架中,操作日志与数据日志的记录流程如下图所示:
这里用文字简单描述一下操作日志与数据日志记录的实现思路:
- 定义了一个“功能信息记录”的实体,用于提取系统中各个功能点的基础信息(名称、MVC的Area-Controller-Action、功能访问类型(匿名访问-登录访问-特定角色访问)、是否启用功能日志,是否启用数据日志、功能URL等),并配置功能的行为
- 定义了一个“实体信息记录”的实体,用于提取系统中各个数据实体类型的基础信息(实体类型全名、实体名称、是否启用数据日志,实体属性信息集),并配置实体的行为
- 系统初始化的时候,通过反射加载的程序集,提取并构建各个功能点(主要是MVC的Controller-Action)的功能信息记录,更新到数据库中
- 系统初始化的时候,通过反射加载的程序集,提取并构建各个实体类型的实体信息记录,更新到数据库中
- 利用MVC框架的ActionFilter进行AOP拦截,定义一个专门用于操作日志记录的 OperateLogFilterAttribute ,重写 OnActionExecuted 方法进行操作日志的记录
- 操作日志与数据日志记录的详细流程如下:
- 在用户的业务操作执行到保存数据的时候(EF执行SaveChanges时),根据操作涉及的实体获取相应的实体信息记录,确定是否创建数据日志,不需创建则跳过
- 需要创建时,根据实体的状态(Added-Modified-Deleted),创建各个实体的新增-更新-删除的数据日志信息,并存储到临时缓存中
- 执行到 OperateLogFilterAttribute 的 OnActionExecuted 方法的时候,根据ActionExecutedContext 中提供的Area,Controller,Action等信息,查询出当前功能的功能信息记录,确定是否记录操作日志,不需记录则返回
- 需要根据功能信息记录,创建操作日志信息,并指定当前用户为日志操作人。
- 根据功能信息是否启用数据日志的配置,确定是否记录数据日志,需要记录时,从临时缓存中提取前面创建的数据日志,作为从数据配置到操作日志中
- 向系统外部保存操作日志信息,完成操作日志的记录
记录各个功能点的功能信息接口定义如下:
1 /// <summary> 2 /// 功能接口,最小功能信息 3 /// </summary> 4 public interface IFunction 5 { 6 /// <summary> 7 /// 获取或设置 功能名称 8 /// </summary> 9 string Name { get; set; } 10 11 /// <summary> 12 /// 获取或设置 区域名称 13 /// </summary> 14 string Area { get; set; } 15 16 /// <summary> 17 /// 获取或设置 控制器名称 18 /// </summary> 19 string Controller { get; set; } 20 21 /// <summary> 22 /// 获取或设置 功能名称 23 /// </summary> 24 string Action { get; set; } 25 26 /// <summary> 27 /// 获取或设置 功能类型 28 /// </summary> 29 FunctionType FunctionType { get; set; } 30 31 /// <summary> 32 /// 获取或设置 是否启用操作日志 33 /// </summary> 34 bool OperateLogEnabled { get; set; } 35 36 /// <summary> 37 /// 获取或设置 是否启用数据日志 38 /// </summary> 39 bool DataLogEnabled { get; set; } 40 41 /// <summary> 42 /// 获取或设置 是否锁定 43 /// </summary> 44 bool IsLocked { get; set; } 45 46 /// <summary> 47 /// 获取或设置 功能地址 48 /// </summary> 49 string Url { get; set; } 50 }
记录各个数据实体类型的实体信息接口定义如下:
1 /// <summary> 2 /// 实体数据接口 3 /// </summary> 4 public interface IEntityInfo 5 { 6 /// <summary> 7 /// 获取 实体数据类型名称 8 /// </summary> 9 string ClassName { get; } 10 11 /// <summary> 12 /// 获取 实体数据显示名称 13 /// </summary> 14 string Name { get; } 15 16 /// <summary> 17 /// 获取 是否启用数据日志 18 /// </summary> 19 bool DataLogEnabled { get; } 20 21 /// <summary> 22 /// 获取 实体属性信息字典 23 /// </summary> 24 IDictionary<string, string> PropertyNames { get; } 25 }
OSharp框架中,已经派生了 Function 与 EntityInfo 两个实体类型,作为功能信息与实体信息的封装。
功能信息与实体信息的初始化实现,主要定义在 FunctionHandlerBase<TFunction, TKey> 与 EntityInfoHandlerBase<TEntityInfo, TKey> 两个基础中,OSharp中已经派生了 public class DefaultFunctionHandler : FunctionHandlerBase<Function, Guid> 与 public class DefaultEntityInfoHandler : EntityInfoHandlerBase<EntityInfo, Guid> 作为系统初始化时,从程序集中提取并更新功能信息与数据信息的默认实现。
由代码图,我们能很直观的看到实体与处理器之间的关系:
关于这两个处理器的实现流程,不是本文的重点,将在后面讲解OSharp初始化实现时再详述,这里先略过。提取的数据展示如下:
提取的功能信息:
提取的实体数据信息:
操作日志实体定义如下:
1 /// <summary> 2 /// 操作日志信息类 3 /// </summary> 4 [Description("系统-操作日志信息")] 5 public class OperateLog : EntityBase<int>, ICreatedTime 6 { 7 /// <summary> 8 /// 初始化一个<see cref="OperateLog"/>类型的新实例 9 /// </summary> 10 public OperateLog() 11 { 12 DataLogs = new List<DataLog>(); 13 } 14 15 /// <summary> 16 /// 获取或设置 执行的功能名称 17 /// </summary> 18 [StringLength(100)] 19 public string FunctionName { get; set; } 20 21 /// <summary> 22 /// 获取或设置 操作人信息 23 /// </summary> 24 public Operator Operator { get; set; } 25 26 /// <summary> 27 /// 获取设置 信息创建时间 28 /// </summary> 29 public DateTime CreatedTime { get; set; } 30 31 /// <summary> 32 /// 获取或设置 数据日志集合 33 /// </summary> 34 public virtual ICollection<DataLog> DataLogs { get; set; } 35 }
数据日志实体定义如下:
1 /// <summary> 2 /// 数据日志信息类 3 /// </summary> 4 [Description("系统-数据日志信息")] 5 public class DataLog : EntityBase<int> 6 { 7 /// <summary> 8 /// 初始化一个<see cref="DataLog"/>类型的新实例 9 /// </summary> 10 public DataLog() 11 : this(null, null, OperatingType.Query) 12 { } 13 14 /// <summary> 15 /// 初始化一个<see cref="DataLog"/>类型的新实例 16 /// </summary> 17 public DataLog(string entityName, string name, OperatingType operatingType) 18 { 19 EntityName = entityName; 20 Name = name; 21 OperateType = operatingType; 22 LogItems = new List<DataLogItem>(); 23 } 24 25 /// <summary> 26 /// 获取或设置 类型名称 27 /// </summary> 28 [StringLength(500)] 29 [Display(Name = "类型名称")] 30 public string EntityName { get; set; } 31 32 /// <summary> 33 /// 获取或设置 实体名称 34 /// </summary> 35 [Display(Name = "实体名称")] 36 public string Name { get; set; } 37 38 /// <summary> 39 /// 获取或设置 数据编号 40 /// </summary> 41 [StringLength(150)] 42 [DisplayName("主键值")] 43 public string EntityKey { get; set; } 44 45 /// <summary> 46 /// 获取或设置 操作类型 47 /// </summary> 48 [Description("操作类型")] 49 public OperatingType OperateType { get; set; } 50 51 /// <summary> 52 /// 获取或设置 操作日志信息 53 /// </summary> 54 public virtual OperateLog OperateLog { get; set; } 55 56 /// <summary> 57 /// 获取或设置 操作明细 58 /// </summary> 59 public virtual ICollection<DataLogItem> LogItems { get; set; } 60 }
数据日志操作变更明细项
1 /// <summary> 2 /// 实体操作日志明细 3 /// </summary> 4 [Description("系统-操作日志明细信息")] 5 public class DataLogItem : EntityBase<Guid> 6 { 7 /// <summary> 8 /// 初始化一个<see cref="DataLogItem"/>类型的新实例 9 /// </summary> 10 public DataLogItem() 11 : this(null, null) 12 { } 13 14 /// <summary> 15 ///初始化一个<see cref="DataLogItem"/>类型的新实例 16 /// </summary> 17 /// <param name="originalValue">旧值</param> 18 /// <param name="newValue">新值</param> 19 public DataLogItem(string originalValue, string newValue) 20 { 21 Id = CombHelper.NewComb(); 22 OriginalValue = originalValue; 23 NewValue = newValue; 24 } 25 26 /// <summary> 27 /// 获取或设置 字段 28 /// </summary> 29 public string Field { get; set; } 30 31 /// <summary> 32 /// 获取或设置 字段名称 33 /// </summary> 34 public string FieldName { get; set; } 35 36 /// <summary> 37 /// 获取或设置 旧值 38 /// </summary> 39 public string OriginalValue { get; set; } 40 41 /// <summary> 42 /// 获取或设置 新值 43 /// </summary> 44 public string NewValue { get; set; } 45 46 /// <summary> 47 /// 获取或设置 数据类型 48 /// </summary> 49 public string DataType { get; set; } 50 51 /// <summary> 52 /// 获取或设置 所属数据日志 53 /// </summary> 54 public virtual DataLog DataLog { get; set; } 55 }
数据日志操作类型的枚举:
1 /// <summary> 2 /// 实体数据日志操作类型 3 /// </summary> 4 public enum OperatingType 5 { 6 /// <summary> 7 /// 查询 8 /// </summary> 9 Query = 0, 10 11 /// <summary> 12 /// 新建 13 /// </summary> 14 Insert = 10, 15 16 /// <summary> 17 /// 更新 18 /// </summary> 19 Update = 20, 20 21 /// <summary> 22 /// 删除 23 /// </summary> 24 Delete = 30 25 }
下图以较直观的方式显示操作日志与数据日志之间的关系:
数据日志,主要记录业务操作过程中涉及到的各个数据实体的变更,而这里的变更,主要是实体的新增、更新、删除三种情况。
在EntityFramework的数据操作中,实体经过业务处理之后,都是有状态跟踪的,即是 EntityState 枚举类型:
1 public enum EntityState 2 { 3 Detached = 1, 4 Unchanged = 2, 5 Added = 4, 6 Deleted = 8, 7 Modified = 16, 8 }
我们要关心的状态,主要是Added、Deleted、Modified三个值,分别对应着新增、删除、更新三种状态,在EntityFramework执行到 SaveChanges 的时候,各个实体的状态已经确定。OSharp将在这个时机获取变更的实体并创建数据日志信息。
1 /// <summary> 2 /// 提交当前单元操作的更改 3 /// </summary> 4 /// <param name="validateOnSaveEnabled">提交保存时是否验证实体约束有效性。</param> 5 /// <returns>操作影响的行数</returns> 6 internal virtual int SaveChanges(bool validateOnSaveEnabled) 7 { 8 bool isReturn = Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled; 9 try 10 { 11 Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled; 12 //记录实体操作日志 13 List<DataLog> logs = new List<DataLog>(); 14 if (DataLoggingEnabled) 15 { 16 logs = this.GetEntityDataLogs().ToList(); 17 } 18 int count = base.SaveChanges(); 19 if (count > 0 && DataLoggingEnabled) 20 { 21 Logger.Info(logs, true); 22 } 23 TransactionEnabled = false; 24 return count; 25 } 26 catch (DbUpdateException e) 27 { 28 if (e.InnerException != null && e.InnerException.InnerException is SqlException) 29 { 30 SqlException sqlEx = e.InnerException.InnerException as SqlException; 31 string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number); 32 throw new OSharpException("提交数据更新时发生异常:" + msg, sqlEx); 33 } 34 throw; 35 } 36 finally 37 { 38 if (isReturn) 39 { 40 Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled; 41 } 42 } 43 }
以上代码中, DataLoggingEnabled 属性 是当前上下文是否开启数据日志的总开关,当开启数据日志记录功能时,才进行数据日志的创建。
创建数据日志的实现如下,主要是从对象管理器中筛选出指定状态的实体对象,再由实体类型全名获取相应实体的“实体信息记录”,确定是否执行数据日志的创建,然后创建数据日志信息:
1 /// <summary> 2 /// 获取数据上下文的变更日志信息 3 /// </summary> 4 public static IEnumerable<DataLog> GetEntityDataLogs(this DbContext dbContext) 5 { 6 ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; 7 ObjectStateManager manager = objectContext.ObjectStateManager; 8 9 IEnumerable<DataLog> logs = from entry in manager.GetObjectStateEntries(EntityState.Added).Where(entry => entry.Entity != null) 10 let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) 11 where entityInfo != null && entityInfo.DataLogEnabled 12 select GetAddedLog(entry, entityInfo); 13 14 logs = logs.Concat(from entry in manager.GetObjectStateEntries(EntityState.Modified).Where(entry => entry.Entity != null) 15 let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) 16 where entityInfo != null && entityInfo.DataLogEnabled 17 select GetModifiedLog(entry, entityInfo)); 18 19 logs = logs.Concat(from entry in manager.GetObjectStateEntries(EntityState.Deleted).Where(entry => entry.Entity != null) 20 let entityInfo = OSharpContext.Current.EntityInfoHandler.GetEntityInfo(entry.Entity.GetType()) 21 where entityInfo != null && entityInfo.DataLogEnabled 22 select GetDeletedLog(entry, entityInfo)); 23 24 return logs; 25 }
创建“新增”实体的数据日志:
1 /// <summary> 2 /// 获取添加数据的日志信息 3 /// </summary> 4 /// <param name="entry">实体状态跟踪信息</param> 5 /// <param name="entityInfo">实体数据信息</param> 6 /// <returns>新增数据日志信息</returns> 7 private static DataLog GetAddedLog(ObjectStateEntry entry, IEntityInfo entityInfo) 8 { 9 DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Insert); 10 for (int i = 0; i < entry.CurrentValues.FieldCount; i++) 11 { 12 string name = entry.CurrentValues.GetName(i); 13 if (name == "Timestamp") 14 { 15 continue; 16 } 17 object value = entry.CurrentValues.GetValue(i); 18 if (name == "Id") 19 { 20 log.EntityKey = value.ToString(); 21 } 22 Type fieldType = entry.CurrentValues.GetFieldType(i); 23 DataLogItem logItem = new DataLogItem() 24 { 25 Field = name, 26 FieldName = entityInfo.PropertyNames[name], 27 NewValue = value == null ? null : value.ToString(), 28 DataType = fieldType == null ? null : fieldType.Name 29 }; 30 log.LogItems.Add(logItem); 31 } 32 return log; 33 }
创建“更新”实体的数据日志:
1 /// <summary> 2 /// 获取修改数据的日志信息 3 /// </summary> 4 /// <param name="entry">实体状态跟踪信息</param> 5 /// <param name="entityInfo">实体数据信息</param> 6 /// <returns>修改数据日志信息</returns> 7 private static DataLog GetModifiedLog(ObjectStateEntry entry, IEntityInfo entityInfo) 8 { 9 DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Update); 10 for (int i = 0; i < entry.CurrentValues.FieldCount; i++) 11 { 12 string name = entry.CurrentValues.GetName(i); 13 if (name == "Timestamp") 14 { 15 continue; 16 } 17 object currentValue = entry.CurrentValues.GetValue(i); 18 object originalValue = entry.OriginalValues[name]; 19 if (name == "Id") 20 { 21 log.EntityKey = originalValue.ToString(); 22 } 23 if (currentValue.Equals(originalValue)) 24 { 25 continue; 26 } 27 Type fieldType = entry.CurrentValues.GetFieldType(i); 28 DataLogItem logItem = new DataLogItem() 29 { 30 Field = name, 31 FieldName = entityInfo.PropertyNames[name], 32 NewValue = currentValue == null ? null : currentValue.ToString(), 33 OriginalValue = originalValue == null ? null : originalValue.ToString(), 34 DataType = fieldType == null ? null : fieldType.Name 35 }; 36 log.LogItems.Add(logItem); 37 } 38 return log; 39 }
创建“删除”实体的数据日志:
1 /// <summary> 2 /// 获取删除数据的日志信息 3 /// </summary> 4 /// <param name="entry">实体状态跟踪信息</param> 5 /// <param name="entityInfo">实体数据信息</param> 6 /// <returns>删除数据日志信息</returns> 7 private static DataLog GetDeletedLog(ObjectStateEntry entry, IEntityInfo entityInfo) 8 { 9 DataLog log = new DataLog(entityInfo.ClassName, entityInfo.Name, OperatingType.Delete); 10 for (int i = 0; i < entry.OriginalValues.FieldCount; i++) 11 { 12 string name = entry.OriginalValues.GetName(i); 13 if (name == "Timestamp") 14 { 15 continue; 16 } 17 object originalValue = entry.OriginalValues[i]; 18 if (name == "Id") 19 { 20 log.EntityKey = originalValue.ToString(); 21 } 22 Type fieldType = entry.OriginalValues.GetFieldType(i); 23 DataLogItem logItem = new DataLogItem() 24 { 25 Field = name, 26 FieldName = entityInfo.PropertyNames[name], 27 OriginalValue = originalValue == null ? null : originalValue.ToString(), 28 DataType = fieldType == null ? null : fieldType.Name 29 }; 30 log.LogItems.Add(logItem); 31 } 32 return log; 33 }
前面我们已经完成了数据日志创建,但数据日志是由数据层的EntityFramework的SaveChanges方法创建的,而创建的数据日志,最终将传递到上层定义的 OperateLogFilterAttribute 中进行使用,这就需要我们通过一定的机制将数据日志往上传递。在这里,使用的是日志组件。
OSharp中定义了一个数据日志缓存,专门用于接收数据层创建的数据日志信息:
1 /// <summary> 2 /// 数据日志缓存接口 3 /// </summary> 4 public interface IDataLogCache : IDependency 5 { 6 /// <summary> 7 /// 获取 数据日志集合 8 /// </summary> 9 IEnumerable<DataLog> DataLogs { get; } 10 11 /// <summary> 12 /// 向缓存中添加数据日志信息 13 /// </summary> 14 /// <param name="dataLog">数据日志信息</param> 15 void AddDataLog(DataLog dataLog); 16 }
在专用于数据日志记录的 DatabaseLog 的 Write 方法重写时,判断数据是否是 DataLog 类型,并存入 IDataLogCache 中,这里使用MVC的依赖注入功能获取IDataLogCache的实现,以保证其在同一Http请求中,获取的是同一实例:
1 /// <summary> 2 /// 获取日志输出处理委托实例 3 /// </summary> 4 /// <param name="level">日志输出级别</param> 5 /// <param name="message">日志消息</param> 6 /// <param name="exception">日志异常</param> 7 /// <param name="isData">是否数据日志</param> 8 protected override void Write(LogLevel level, object message, Exception exception, bool isData = false) 9 { 10 if (!isData) 11 { 12 return; 13 } 14 IEnumerable<DataLog> dataLogs = message as IEnumerable<DataLog>; 15 if (dataLogs == null) 16 { 17 return; 18 } 19 IDataLogCache logCache = DependencyResolver.Current.GetService<IDataLogCache>(); 20 foreach (DataLog dataLog in dataLogs) 21 { 22 logCache.AddDataLog(dataLog); 23 } 24 }
定义了一个 OperateLogFilterAttribute 的ActionFilter,专门用于拦截并记录操作日志。
1 /// <summary> 2 /// 操作日志记录过滤器 3 /// </summary> 4 public class OperateLogFilterAttribute : ActionFilterAttribute 5 { 6 /// <summary> 7 /// 获取或设置 数据日志缓存 8 /// </summary> 9 public IDataLogCache DataLogCache { get; set; } 10 11 /// <summary> 12 /// 获取或设置 操作日志输出者 13 /// </summary> 14 public IOperateLogWriter OperateLogWriter { get; set; } 15 16 /// <summary> 17 /// Called after the action method executes. 18 /// </summary> 19 /// <param name="filterContext">The filter context.</param> 20 public override void OnActionExecuted(ActionExecutedContext filterContext) 21 { 22 string area = filterContext.GetAreaName(); 23 string controller = filterContext.GetControllerName(); 24 string action = filterContext.GetActionName(); 25 26 IFunction function = OSharpContext.Current.FunctionHandler.GetFunction(area, controller, action); 27 if (function == null || !function.OperateLogEnabled) 28 { 29 return; 30 } 31 Operator @operator = new Operator() 32 { 33 Ip = filterContext.HttpContext.Request.GetIpAddress(), 34 }; 35 if (filterContext.HttpContext.Request.IsAuthenticated) 36 { 37 ClaimsIdentity identity = filterContext.HttpContext.User.Identity as ClaimsIdentity; 38 if (identity != null) 39 { 40 @operator.UserId = identity.GetClaimValue(ClaimTypes.NameIdentifier); 41 @operator.Name = identity.GetClaimValue(ClaimTypes.Name); 42 @operator.NickName = identity.GetClaimValue(ClaimTypes.GivenName); 43 } 44 } 45 46 OperateLog operateLog = new OperateLog() 47 { 48 FunctionName = function.Name, 49 Operator = @operator 50 }; 51 if (function.DataLogEnabled) 52 { 53 foreach (DataLog dataLog in DataLogCache.DataLogs) 54 { 55 operateLog.DataLogs.Add(dataLog); 56 } 57 } 58 OperateLogWriter.Write(operateLog); 59 } 60 }
最后,操作日志将由 IOperateLogWriter 进行输出,定义如下:
1 /// <summary> 2 /// 操作日志输出接口 3 /// </summary> 4 public interface IOperateLogWriter : IDependency 5 { 6 /// <summary> 7 /// 输出操作日志 8 /// </summary> 9 /// <param name="operateLog">操作日志信息</param> 10 void Write(OperateLog operateLog); 11 }
默认的,操作日志将被记录到数据库中:
1 /// <summary> 2 /// 操作日志数据库输出实现 3 /// </summary> 4 public class DatabaseOperateLogWriter : IOperateLogWriter 5 { 6 private readonly IRepository<OperateLog, int> _operateLogRepository; 7 8 /// <summary> 9 /// 初始化一个<see cref="DatabaseOperateLogWriter"/>类型的新实例 10 /// </summary> 11 public DatabaseOperateLogWriter(IRepository<OperateLog, int> operateLogRepository) 12 { 13 _operateLogRepository = operateLogRepository; 14 } 15 16 /// <summary> 17 /// 输出操作日志 18 /// </summary> 19 /// <param name="operateLog">操作日志信息</param> 20 public void Write(OperateLog operateLog) 21 { 22 operateLog.CheckNotNull("operateLog" ); 23 _operateLogRepository.Insert(operateLog); 24 } 25 }
如果一条操作日志中包含有数据日志,那么数据日志将以下级数据的方式展现在操作日志中:
OSharp项目已在github.com上开源,地址为:https://github.com/i66soft/osharp,欢迎阅读代码,欢迎 Watch(关注),欢迎 Star(推荐),如果您认同 OSharp 项目的设计思想,欢迎参与 OSharp 项目的开发。
在Visual Studio 2013中,可直接获取 OSharp 的最新源代码,获取方式如下,地址为:https://github.com/i66soft/osharp.git
很多童鞋想参与开源项目,为项目做贡献,但又不知道如何做,这里我简单说下参与OSharp的步骤吧:
OSharp的相关类库已经发布到nuget上,欢迎试用,直接在nuget上搜索 “osharp” 关键字即可找到
本文已同步到系列目录:OSharp快速开发框架解说系列