轻量级 C# 可复用系统架构的研究与总结 (MVX+MEF+EF6)
0 前言
在企业级应用开发周期中,普遍存在随着应用程序规模扩大、版本更迭、甚至人员不稳定等带来整个应用程序系统代码结构混乱不堪、重复代码爆炸、超级函数量陡升等现象,最终影响项目按时交付和验收。笔者在经历过许多这样的开发历程过后,简单的整理形成本文。
1 概述
本应用程序基础架构是通用型数据处理应用程序(系统)代码架构,采用轻量级架构,适用于团队项目的开展和版本迭代。在整套架构结构中,广泛采用IOC结构,采用精简和可重用的模板代码替代反复重叠且容易出错的工作。
2 结构说明
2.1 应用程序包(子项目)结构
如上图所示,项目分为
- UI(界面)
- DataEntity(数据实体)
- IDataBiz(数据操作业务接口)
- IDataOperate(数据操作接口)
- DataBiz(数据业务实现)
- DataOperate(数据操作实现)
- Utils(工具集)
共七部分。其中,各包之间依赖关系如下:
由上图可见,数据访问实现模块与业务模块之间不存在依赖关系,各实现模块均依赖于其相对应的抽象接口模块;而依赖仅存在于接口之间(除实体模块和工具模块外),而UI模块仅依赖于数据访问接口。
3 程序集(模块)设计
本章描述各程序集的主要设计思路,通过简化的代码与说明,阐述各程序集实现细则。由于Utils程序集受到其他各实现模块的引用,所以本章先从Utils开始,顺序依次为Utils->Entity->IDataOperate->IDataBiz->DateOperate->DataBiz,本文不对UI部分进行描述。
3.1 Utils模块
Utils 模块为整个系统提供基础算法支持、对象类型转化、对象克隆、字符串处理等各种功能函数。本文使用三个工具类:实体对象工具(EntityObjectUtil)、KMP算法工具(KMPUtil,用于快速匹配大量字符串)、对象深拷贝工具(ObjectCloneUtil)。具体代码如下:
3.1.1 实体对象工具类
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenTCM.Utils { public class EntityObjUtils { ////// 获取对象所有属性键值对 /// /// 对象类型 /// 目标对象 /// 对象键值对集 public Dictionary<string, object> GetObjProperties (T t) { Dictionary<string, object> res = new Dictionary<string, object>(); if (t == null) { return null; } var properties = t.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); if (properties.Length <= 0) { return null; } foreach (var item in properties) { string name = item.Name; object value = item.GetValue(t, null); if (item.PropertyType.IsValueType || item.PropertyType.Name.StartsWith("String")) { res.Add(name, value); } else { GetObjProperties(value); } } return res; } /// /// 实体类转SQL where 子串(不包含Where) /// 实体类必须可序列化,属性必须仅为值类型和字符串类型(不支持其他类型) /// /// 实体类对象类型,必须实现IEquatable接口 /// 作为条件的实体类 /// 各属性间逻辑运算符(如AND,OR) /// 是否为模糊查询 /// SQL条件部分语句 public string EntityToSqlCondition (T entity, string logicOperator = "and", bool isFuzzy = false) where T:class, IEquatable , new() { if (entity == null) { return null; } if (entity.Equals(new T())) { return null; } //对象属性序列 var properties = entity.GetType() .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) .AsEnumerable().Where((p) => { bool res = p.GetValue(entity, null) != null; if (res) { res = !string.IsNullOrWhiteSpace(p.GetValue(entity, null).ToString()); } return res; }); if (properties.Count() <= 0) { return null; } StringBuilder sbCondition = new StringBuilder(" "); int index = 0; foreach (var item in properties) { string propName = item.Name; object propValue = item.GetValue(entity, null); if (index != properties.Count() - 1)//不是最后一个属性 { if (item.PropertyType.IsValueType) { sbCondition.AppendFormat("{0}={1} {2} ", propName, propValue, logicOperator); } else if (item.PropertyType.Name.StartsWith("String")) { if (propValue.ToString().IndexOf('\'') != -1) {//SQL注入 throw new FormatException("Detect risk of SQL injection in this entity class"); } if (isFuzzy)//生成模糊查询 { sbCondition.AppendFormat("{0} like '%{1}%' {2} ", propName, propValue, logicOperator); } else { sbCondition.AppendFormat("{0}='{1}' {2} ", propName, propValue, logicOperator); } } } else//最后一个属性 { if (item.PropertyType.IsValueType) { sbCondition.AppendFormat(" {0}={1} ", propName, propValue); } else if (item.PropertyType.Name.StartsWith("String")) { if (propValue.ToString().IndexOf('\'') != -1) {//SQL注入 throw new FormatException("Detect risk of SQL injection in this entity class"); } if (isFuzzy)//生成模糊查询 { sbCondition.AppendFormat(" {0} like '%{1}%' ", propName, propValue); } else { sbCondition.AppendFormat(" {0}='{1}' ", propName, propValue); } } } index++; } return sbCondition.ToString(); } /// /// 是否存在SQL注入敏感因素(参数为空同样返回True) /// /// 字符串样本 /// 存在:True, 不存在:False public bool DetectSQLInj(string strProp) { if (string.IsNullOrWhiteSpace(strProp)) { return true;//不存在注入风险 } string lower = strProp.ToLower(); bool res = false; if (strProp.Length < 4) { return false; } if (strProp.Length < 1024)//少量数据用IndexOf { res = lower.IndexOf("exec") != -1 || lower.IndexOf("declare") != -1 || lower.IndexOf("insert") != -1 || lower.IndexOf("delete") != -1 || lower.IndexOf("update") != -1 || lower.IndexOf("select") != -1; } else//大量数据用KMP算法 { res = KMPUtil.GetAllOccurences("exec", strProp).Count > 0 || KMPUtil.GetAllOccurences("declare", strProp).Count > 0 || KMPUtil.GetAllOccurences("insert", strProp).Count > 0 || KMPUtil.GetAllOccurences("delete", strProp).Count > 0 || KMPUtil.GetAllOccurences("update", strProp).Count > 0 || KMPUtil.GetAllOccurences("select", strProp).Count > 0; } if (res) { throw new FormatException("Detect risk of SQL injection in this entity class"); } return res; } /// /// 比较不依赖其他实体的实体对象是否相等 /// /// 实体类型 /// 实体类A /// 实体类B /// 是否相等 public bool EntityEquals (T entityA, T entityB) where T : new() { if (entityA == null || entityB == null) { return false; } var entA = GetObjProperties(entityA); var entB = GetObjProperties(entityB); if (entA.Count != entB.Count || entA.Count == 0 || entB.Count == 0) { return false; } foreach (var para in entA) { string nameA = para.Key; object valueA = para.Value; if (!entB.Keys.Contains(nameA)) { return false; } object valueB = entB[nameA]; if (valueA == null || valueB == null) { if (valueA == null && valueB == null) { continue; } else { return false; } } else if (valueA.ToString() != valueB.ToString()) { return false; } } return true; } } }
3.1.2 KMP算法工具类
namespace OpenTCM.Utils { public sealed class KMPUtil { private KMPUtil() { } ////// Finds all the occurences a pattern in a a string /// /// The pattern to search for /// The target string to search for /// /// Return an Arraylist containing the indexs where the /// patternn occured /// public static List<int> GetAllOccurences(string pattern, string targetString) { return GetOccurences(pattern, targetString); } /// /// Finds all the occurences a pattern in a string in reverse order /// /// The pattern to search for /// /// The target string to search for. This string is actually reversed /// /// /// Return an Arraylist containing the indexs where the /// patternn occured /// public static List<int> GetOccurencesForReverseString(string pattern, string targetString) { char[] array = pattern.ToCharArray(); Array.Reverse(array); return GetOccurences(new string(array), targetString); } /// /// Finds all the occurences a pattern in a a string /// /// The pattern to search for /// The target string to search for /// /// Return an Arraylist containing the indexs where the /// patternn occured /// private static List<int> GetOccurences(string pattern, string targetString) { List<int> result; int[] transitionArray; char[] charArray; char[] patternArray; charArray = targetString.ToLower().ToCharArray(); patternArray = pattern.ToLower().ToCharArray(); result = new List<int>(); PrefixArray prefixArray = new PrefixArray(pattern); transitionArray = prefixArray.TransitionArray; //Keeps track of the pattern index int k = 0; for (int i = 0; i < charArray.Length; i++) { //If there is a match if (charArray[i] == patternArray[k]) { //Move the pattern index by one k++; } else { //There is a mismatch..so move the pattern //The amount to move through the pattern int prefix = transitionArray[k]; //if the current char does not match //the char in the pattern that concides //when moved then shift the pattern entirley, so //we dont make a unnecssary comparision if (prefix + 1 > patternArray.Length && charArray[i] != patternArray[prefix + 1]) { k = 0; } else { k = prefix; } } //A complet match, if kis //equal to pattern length if (k == patternArray.Length) { //Add it to our result result.Add(i - (patternArray.Length - 1)); //Set k as if the next character is a mismatch //therefore we dont mis out any other containing //pattern k = transitionArray[k - 1]; } } return result; } } public class PrefixArray { /// /// The pattern to compute the /// array /// private string pattern; private int[] hArray; /// /// Constructs a prefix array /// /// /// The to be used to construct /// the prefix array public PrefixArray(string pattern) { if (pattern == null || pattern.Length == 0) { throw new ArgumentException ("The pattern may not be null or 0 lenght", "pattern"); } this.pattern = pattern; hArray = new int[pattern.Length]; ComputeHArray(); } /// /// Computes the prefix array /// private void ComputeHArray() { /*Array to keep track of the sub string in each iteration*/ char[] temp = null; //An array containing the characters of the string char[] patternArray = pattern.ToCharArray(); //The first character in the string... //At this point the patern length is validated to be atleast 1 char firstChar = patternArray[0]; //This defaults to 0 hArray[0] = 0; for (int i = 1; i < pattern.Length; i++) { temp = SubCharArray(i, patternArray); hArray[i] = GetPrefixLegth(temp, firstChar); } } private static int GetPrefixLegth(char[] array, char charToMatch) { for (int i = 2; i < array.Length; i++) { //if it is a match if (array[i] == charToMatch) { if (IsSuffixExist(i, array)) { //Return on the first prefix which is the largest return array.Length - i; } } } return 0; } /// /// Tests whether a suffix exists from the specified index /// /// /// The index of the char[] to start looking /// for the prefix /// /// The source array /// /// A bool; true if a prefix exist at the /// specified pos private static bool IsSuffixExist(int index, char[] array) { //Keep track of the prefix index int k = 0; for (int i = index; i < array.Length; i++) { //A mismatch so return if (array[i] != array[k]) { return false; } k++; } return true; } /// /// Creates a sub char[] from the source array /// /// /// The end index to until which /// the copying should occur /// The source array /// A sub array private static char[] SubCharArray(int endIndex, char[] array) { char[] targetArray = new char[endIndex + 1]; for (int i = 0; i <= endIndex; i++) { targetArray[i] = array[i]; } return targetArray; } /// /// Gets the transition array /// public int[] TransitionArray { get { return hArray; } } } }
3.1.3 对象克隆工具类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Serialization; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; namespace OpenTCM.Utils { public class ObjectCloneUtil { ////// 深度克隆对象 /// /// 待克隆类型 /// 待克隆对象 /// 新对象 public static T Clone (T obj) { T ret = default(T); if (obj != null) { XmlSerializer cloner = new XmlSerializer(typeof(T)); MemoryStream stream = new MemoryStream(); cloner.Serialize(stream, obj); stream.Seek(0, SeekOrigin.Begin); ret = (T)cloner.Deserialize(stream); } return ret; } /// /// 克隆对象 /// /// 待克隆对象 /// 新对象 public static object CloneObject(object obj) { using (MemoryStream memStream = new MemoryStream()) { BinaryFormatter binaryFormatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone)); binaryFormatter.Serialize(memStream, obj); memStream.Seek(0, SeekOrigin.Begin); return binaryFormatter.Deserialize(memStream); } } } }
3.2 实体模块
实体模块负责映射数据库数据表结构,每张表对应一个实体类。
注意:数据库各字段建议仅为字符串类型或数值类型,日期类型建议使用Int 64数据类型存储(格式为:yyyyMMddHHmmssfff),这样的设计有助于优化数据查询速度。本文采用的示例仅支持上述类型,读者可根据自身实际情况采纳本文思想,而修改其具体实现方案。
注意:实体类存在一对一、一对多、多对多等关系,但本文中实体类仅描述原始的数据库表结构,实体类之间不存在依赖关系。数据关联通过程序逻辑判断。(目的在于降低耦合,避免循环依赖)。
实体模块用于存放各数据库表映射的实体类,实体模块中为各实体类提供了统一管理接口。命名为I
本示例模拟为中医院药材提供进销存管理功能,项目名为OpenTCM,实体模块各类关系如下:
IOpenTCMEntity接口仅用于规范实体类,并对实体类进行统一标记。该接口必须遵循如下约定:
- 必须为泛型接口。
- 泛型类型必须为其自身。
- 泛型类型通过where约束为class和new(),指定实体类必须是类类型,并具有无参的个构造函数。
- 该接口可以不声明任何函数或属性。
这样可以约束实体类,并通过统一实现某接口(如IEquatable
对实体类使用接口约束,可以便于其他模块对实体模块的抽象引用。即:不引用具体实体,而引用接口,这主要用于声明泛型类型,详情参见后文。
其中各部分代码如下:
3.2.1 IOpenTCMEntity
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenTCM.DataEntity { ////// 实体类实现接口 /// 实体类必须仅包含数值类型和字符串类型 /// /// 实体对象类型 public interface IOpenTCMEntity : IEquatable where T : class, IOpenTCMEntity , new() { } }
3.2.2 Herb(草药)
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenTCM.DataEntity { ////// 中草药药材 /// TODO: /// https://so.gushiwen.org/guwen/book_12.aspx /// [Serializable] public class Herb : IOpenTCMEntity { public Herb() { } /// /// 主键ID /// public string ID { get; set; } /// /// 原料名,不重复 /// public string HerbName { get; set; } /// /// 释名『多个资料用JSON表示』 /// public string GenericName { get; set; } /// /// 外键,HerbsSupplier /// public string HerbsSupplierID { get; set; } /// /// 参考资料『多个资料用JSON表示』 /// public string TcmReferences { get; set; } /// /// 性味『多个资料用JSON表示』 /// public string MaterialOdour { get; set; } /// /// 适用症『多个资料用JSON表示』 /// public string ApplicableSymptoms { get; set; } /// /// 附方『多个资料用JSON表示』 /// public string AncillaryPrescription { get; set; } /// /// 药材编号 /// public string PackingNo { get; set; } /// /// 最后一次修改时间 /// public long? LastModifiedTime { get; set; } /// /// 本记录状态 /// public int? RecordStatus { get; set; } /// /// 乐观锁版本号 /// public int? DataVersion { get; set; } public bool Equals(Herb other) { if (other == null) { return false; } if (other == this) { return true; } bool res = other.ID == ID && other.HerbName == HerbName && other.GenericName == GenericName && other.HerbsSupplierID == HerbsSupplierID && other.TcmReferences == TcmReferences && other.MaterialOdour == MaterialOdour && other.ApplicableSymptoms == ApplicableSymptoms && other.AncillaryPrescription == AncillaryPrescription && other.PackingNo == PackingNo && other.LastModifiedTime == LastModifiedTime && other.RecordStatus == RecordStatus && other.DataVersion == DataVersion; return res; } } }
3.3 数据访问接口模块
数据访问接口模块声明了对各数据库表的访问操作,本接口不提供任何实现方式,仅声明数据库CRUD相关函数。本模块包含一个基础泛型接口,该泛型类型为实体类接口即IOpenTCMEntity。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.DataEntity; namespace OpenTCM.IDataOperate { ////// 提供基本的数据增删改查声明 /// /// 数据库实体类 public interface IBaseDAO where E : class, IOpenTCMEntity , new() { /// /// 是否存在ID所对应的记录 /// /// /// bool Exists(string id); /// /// 是否存在参数对象所描述的记录(属性为空表示该属性不作为查询条件) /// /// /// bool Exists(E condition); /// /// 将参数描述的实体对象添加到数据库中 /// /// /// bool AddEntity(E po); /// /// 将参数描述的实体对象集合添加到数据库中 /// /// /// int AddEntity(List pos); /// /// 根据ID从数据库中删除实体 /// /// /// bool DelEntity(string id); /// /// 依据参数描述的数据库记录更新数据库, /// 本操作需要先从数据库获取具体的对象。 /// /// /// bool UpdateEntity(E po); /// /// 依据参数描述的对象为条件,获取记录数。 /// /// /// long GetCount(E condition); /// /// 获取总记录数 /// /// long GetCount(); /// /// 获取所有实体对象集合 /// /// List QueryEntity(); /// /// 获取分页对象集合 /// /// /// /// List QueryEntity(int beg, int len); /// /// 根据实体条件分页查询 /// /// 条件实体对象 /// 开始位置 /// 数据条数 /// List QueryEntity(E condition, int beg, int len); /// /// 根据ID获取一个实体对象 /// /// 主键ID字段 /// E QueryEntity(string id); /// /// 依据条件获取实体集合 /// /// 条件实体 /// List QueryEntity(E condition); /// /// 根据条模糊查询 /// /// 条件实体 /// List FuzzyQuery(E condition); List FuzzyQuery(E condition, int beg, int len); } }
数据库各表对应的操作接口均继承自该基础接口,以实现高级抽象,从而达到代码复用性目的。
数据库Herb表对应的操作接口代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.DataEntity; namespace OpenTCM.IDataOperate { public interface IHerbDAO : IBaseDAO{ } }
通过以上代码,IHerbDAO实现自IBaseDAO,并传递Herb实体类作为泛型参数。通过上述代码分析可理解IBaseDAO
3.4 业务接口模块
业务接口模块用于存放包括:数据库访问业务和其他自定义业务,本文仅描述数据操作业务内容,其他内容扩展方式会在本章进行说明。
业务接口模块使用一个高级抽象的基础接口:IBaseBO,业务访问接口模块依赖于数据库访问接口和实体数据接口,所以本基础接口需要两个泛型参数:IOpenTCMEntity和IBaseDAO,接口代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.IDataOperate; using OpenTCM.DataEntity; namespace OpenTCM.IDataBiz { ////// 业务基础接口 /// /// 实体类 /// 持久化类 public interface IBaseBO where E : class, IOpenTCMEntity , new() where DAO : IBaseDAO { /// /// 是否存在ID所对饮的记录 /// /// /// bool Exists(string id); /// /// 是否存在参数对象所描述的记录(属性为空表示该属性不作为查询条件) /// /// /// bool Exists(E condition); /// /// 将参数描述的实体对象添加到数据库中 /// /// /// bool AddEntity(E po); /// /// 将参数描述的实体对象集合添加到数据库中 /// /// /// int AddEntity(List pos); /// /// 根据ID从数据库中删除实体 /// /// /// bool DelEntity(string id); /// /// 依据参数描述的数据库记录更新数据库, /// 本操作需要先从数据库获取具体的对象。 /// /// /// bool UpdateEntity(E po); /// /// 依据参数描述的对象为条件,获取记录数。 /// /// /// long GetCount(E condition); /// /// 获取总记录数 /// /// long GetCount(); /// /// 获取所有实体对象集合 /// /// List QueryEntity(); /// /// 获取分页对象集合 /// /// /// /// List QueryEntity(int beg, int len); /// /// 根据ID获取一个实体对象 /// /// /// E QueryEntity(string id); /// /// 依据条件获取实体集合 /// /// /// List QueryEntity(E condition); /// /// 按实体条件查询并分页 /// /// 实体条件 /// 起始位置 /// 每页数据量 /// List QueryEntity(E condition, int beg, int len); /// /// 模糊查询 /// /// /// List FuzzyQuery(E condition); /// /// 根据实体条件模糊查询,并分页 /// /// 实体条件 /// 起始位置 /// 每页数据量 /// List FuzzyQuery(E condition, int beg, int len); } }
其他数据库表对应的业务接口均实现自该基础接口,Herbs表对应业务接口代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.DataEntity; using OpenTCM.IDataOperate; namespace OpenTCM.IDataBiz { public interface IHerbBO : IBaseBO{ } }
通过上述代码,子接口通过传递具体类型泛型参数,实现对父类的进一步具象化。
3.5 数据库访问实现模块
本节描述数据库访问实现模块,是对数据库访问接口的终极具象化。通过上文图中说明可知,数据库访问模块仅使用实体模块和工具模块。
注意:本节描述的数据库访问实现模块,使用EF6作为数据库访问中间层,但考虑到EF6的内部逻辑,本文仅用EF6作为中间层,但尽量小规模采用EF6的ORM特性,即能用SQL的地方就用SQL,适合用ORM的地方再用ORM。
EF6需要用到数据库上下文,所以本模块定义一个OpenTCMContext作为数据上下文类。代码如下:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; using System.Data.SQLite; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using OpenTCM.DataEntity; using System.Data.SQLite.EF6; using System.Data.SQLite.Linq; using System.Data.SQLite.Generic; using System.ComponentModel.Composition; namespace OpenTCM.DataOperate { public class OpenTCMContext : DbContext { public OpenTCMContext() : base("OpenTCM") { } public DbSetHerbDataSet { get; set; } } }
数据库访问类与数据库访问接口存在实现关系。但由于数据库操作无外乎增删改查等固定操作,针对每张表都需要重复的代码,这样不利于集中管理。鉴于此,本规范描述了一种尽可能集中化的项目架构规范。基本结构如下:
从上图可了解到,IBaseDAO定义了所有数据库操作的动作规范,即各类型增删改查动作。
IHerbDAO接口继承自接口IBaseDAO,并以Herb实体类作为其父接口IBaseDAO的泛型参数,以此确定IHerbDAO为针对Herbs表的操作接口。
而ACommonDAO提供了IBaseDAO描述的抽象函数的通用的默认实现方式(通过抽象的TableName标识不同数据库操作SQL,由EF进行封装,以抽象的EntityDataSet提供访问)。
HerbDAO实现类继承自ACommonDAO——为继承其默认函数实现;其实现自IHerbDAO,以作为其自身的导出标记接口(参阅MEF的Export概念)。
IOpenTCMEntity接口全程作为抽象层泛型参数声明占位,其实现类以Herb为例,作为抽象层次向实现层次过度的泛型参数(如IHerbDAO)。
注意:上述数据库访问的架构方式在功能上可以仅用ACommonDAO扮演接口角色,但为减少对具体实现的完全依赖(ACommonDAO有太多具体的默认实现),仍要求HerbDAO继承自没有任何函数实现的IHerbDAO接口。
根据以上类图,代码示例如下:
3.5.1 ACommonDAO代码示例
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.IDataOperate; using OpenTCM.DataEntity; using System.ComponentModel.Composition; using System.Data.Entity; using OpenTCM.Utils; using System.ComponentModel.Composition.Hosting; using System.Reflection; namespace OpenTCM.DataOperate { ////// 通用数据库访问抽象类 /// 本类提供抽象且通用的数据库访问功能 /// /// 实体类 public abstract class ACommonDAO : IBaseDAO where Entity : class, IOpenTCMEntity , new() { public ACommonDAO() { Utils = new EntityObjUtils(); Context = new OpenTCMContext(); } protected readonly EntityObjUtils Utils;//工具对象 protected readonly OpenTCMContext Context;//EF6数据上下文 /// /// 数据库表名(用于生成SQL) /// protected abstract string TableName { get; set; } /// /// 该数据库访问对象对应的数据库表上下文数据集 /// protected abstract DbSet EntityDataSet { get; set; } /// /// 数据库是否存在ID所对应记录 /// /// /// public bool Exists(string id) { if (Utils.DetectSQLInj(id)) { //TODO: Log the sql attack record return false; } string sql = string.Format("select count(1) from {0} where ID = '{1}'", TableName, id); long count = Context.Database.SqlQuery<long>(sql).FirstOrDefault(); return count > 0; } /// /// 数据库是否存在实体所描述记录 /// /// /// public bool Exists(Entity condition) { string strWhere = Utils.EntityToSqlCondition (condition); if (string.IsNullOrWhiteSpace(strWhere)) { return false; } string sql = string.Format("select count(1) from {0} where {1}", TableName, strWhere); long count = Context.Database.SqlQuery<long>(sql).FirstOrDefault(); return count > 0; } /// /// 将实体对象添加到数据库 /// /// /// public bool AddEntity(Entity po) { EntityDataSet.Add(po); int res = Context.SaveChanges(); return res > 0; } /// /// 将实体集合添加到数据库 /// /// /// public int AddEntity(List pos) { EntityDataSet.AddRange(pos); int res = Context.SaveChanges(); return res; } /// /// 从数据库中删除ID所对应数据 /// /// /// public bool DelEntity(string id) { if (Utils.DetectSQLInj(id)) { //TODO: Log the sql attack record return false; } string sql = string.Format("delete from {0} where ID ='{1}'", TableName, id); int res = Context.Database.ExecuteSqlCommand(sql); return res > 0; } /// /// 按实体对象的描述更新数据库(实体对象必须有真实ID) /// /// /// public bool UpdateEntity(Entity po) { EntityDataSet.Attach(po); Context.Entry(po).State = EntityState.Modified; int res = Context.SaveChanges(); return res > 0; } /// /// 根据实体对象的描述,查找数据库中对应的数据记录数 /// /// /// public long GetCount(Entity condition) { string strWhere = Utils.EntityToSqlCondition (condition); if (Utils.DetectSQLInj(strWhere)) { //TODO: Log the sql attack record return 0; } string sql = string.Format("select count(1) from {0} where {1}", TableName, strWhere); long count = Context.Database.SqlQuery<long>(sql).FirstOrDefault(); return count; } /// /// 查找本表所有数据记录数 /// /// public long GetCount() { string sql = "select count(1) from " + TableName; long count = Context.Database.SqlQuery<long>(sql).FirstOrDefault(); return count; } /// /// 以实体对象方式获取本表所有数据 /// /// public List QueryEntity() { var ls = EntityDataSet.ToList(); return ls; } /// /// 根据开始位置和数量获取分页数据 /// /// /// /// public List QueryEntity(int beg, int len) { var subLs = EntityDataSet.ToList().Skip(beg).Take(len); return subLs.ToList(); } /// /// 根据ID值获取唯一的实体对象 /// /// /// public Entity QueryEntity(string id) { if (Utils.DetectSQLInj(id)) { //TODO: Log the sql attack record return null; } var ls = EntityDataSet.SqlQuery(string.Format("select * from {0} where ID='{1}'", TableName, id)); return ls.FirstOrDefault(); } /// /// 根据实体对象的描述,查找实体数据列表 /// /// /// public List QueryEntity(Entity condition) { string strWhere = Utils.EntityToSqlCondition (condition); if (Utils.DetectSQLInj(strWhere)) { //TODO: Log the sql attack record return new List (); } string sql = string.Format("select * from {0} where {1}", TableName, strWhere); var res = EntityDataSet.SqlQuery(sql); return res.ToList(); } /// /// 根据实体对象的描述,直行模糊查询 /// /// /// public List FuzzyQuery(Entity condition) { string strWhere = Utils.EntityToSqlCondition (condition); if (Utils.DetectSQLInj(strWhere)) { //TODO: Log the sql attack record return new List (); } string sql = string.Format("select * from {0} where {1}", TableName, strWhere); var res = EntityDataSet.SqlQuery(sql); return res.ToList(); } /// /// 根据实体查询数据并分页 /// /// 实体条件 /// 起始位置 /// 每页容量 /// public List QueryEntity(Entity condition, int beg, int len) { string strWhere = Utils.EntityToSqlCondition (condition); if (Utils.DetectSQLInj(strWhere)) { //TODO: Log the sql attack record return new List (); } string sql = string.Format("select * from {0} where {1}", TableName, strWhere); var res = EntityDataSet.SqlQuery(sql).ToList().Skip(beg).Take(len); return res.ToList(); throw new NotImplementedException(); } /// /// 根据实体模糊查询数据并分页 /// /// 实体条件 /// 起始位置 /// 每行数据容量 /// public List FuzzyQuery(Entity condition, int beg, int len) { string strWhere = Utils.EntityToSqlCondition (condition); if (Utils.DetectSQLInj(strWhere)) { //TODO: Log the sql attack record return new List (); } string sql = string.Format("select * from {0} where {1}", TableName, strWhere); var res = EntityDataSet.SqlQuery(sql).Skip(beg).Take(len); return res.ToList(); } } }
3.5.2 HerbDAO代码示例
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.IDataOperate; using OpenTCM.DataEntity; using System.ComponentModel.Composition; using System.Data.Entity; using OpenTCM.Utils; using System.ComponentModel.Composition.Hosting; using System.Reflection; namespace OpenTCM.DataOperate { using Entity = Herb;//实体对象类型别名 [Export(typeof(IHerbDAO))] public class HerbDAO :ACommonDAO, IHerbDAO { public HerbDAO() { TableName = "Herbs"; } protected override string TableName { get; set; } protected override DbSet EntityDataSet { get { return Context.HerbDataSet; } set { Context.HerbDataSet = value; } } } }
3.6 业务实现模块
业务实现模块目的在于对数据库访问的进一步封装,但根据依赖倒转原则,业务实现模块不直接依赖于数据访问实现模块,而依赖于数据访问接口。通过MEF将依赖注入到业务实现模块中。业务实现模块中各组件与其他模块依赖如下:
上图与数据访问实现模块基本结构与设计思路类似。限于篇幅将UML类图调整一下。
详细代码如下:
3.6.1 ACommonBO
ACommonBO类封装了基本的数据库接口调用的通用实现。其具有两个泛型参数Entity和IDAO,Entity是IOpenTCMEntity
using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenTCM.DataEntity; using OpenTCM.IDataBiz; using OpenTCM.IDataOperate; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.Reflection; namespace OpenTCM.DataBiz { public abstract class ACommonBO: IBaseBO where Entity : class, IOpenTCMEntity , new() where IDAO : IBaseDAO { public ACommonBO() { var catalog = new DirectoryCatalog("./"); var container = new CompositionContainer(catalog); container.ComposeParts(this); } /// /// 数据库操作接口(依赖注入,不需要手动实现) /// protected abstract IDAO DbOperator { get; set; } public virtual bool Exists(string id) { return DbOperator.Exists(id); } public virtual bool Exists(Entity condition) { return DbOperator.Exists(condition); } public virtual bool AddEntity(Entity po) { return DbOperator.AddEntity(po); } public virtual int AddEntity(List pos) { return DbOperator.AddEntity(pos); } public virtual bool DelEntity(string id) { return DbOperator.DelEntity(id); } public virtual bool UpdateEntity(Entity po) { return DbOperator.UpdateEntity(po); } public virtual long GetCount(Entity condition) { return DbOperator.GetCount(condition); } public virtual long GetCount() { return DbOperator.GetCount(); } public virtual List QueryEntity() { return DbOperator.QueryEntity(); } public virtual List QueryEntity(int beg, int len) { return DbOperator.QueryEntity(beg, len); } public virtual Entity QueryEntity(string id) { return DbOperator.QueryEntity(id); } public virtual List QueryEntity(Entity condition) { return DbOperator.QueryEntity(condition); } public virtual List FuzzyQuery(Entity condition) { return DbOperator.FuzzyQuery(condition); } public virtual List QueryEntity(Entity condition, int beg, int len) { return DbOperator.QueryEntity(condition, beg, len); } public virtual List FuzzyQuery(Entity condition, int beg, int len) { return DbOperator.FuzzyQuery(condition, beg, len); } } }
3.6.2 HerbBO
HerbBO继承自AcommonBO抽象类,以继承ACommonBO的默认接口函数实现,当业务需要其他的实现方式,可以自行重写父类函数;实现自IHerbBO接口,用以标记导出接口。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using OpenTCM.DataEntity; using OpenTCM.IDataBiz; using OpenTCM.IDataOperate; namespace OpenTCM.DataBiz { [Export(typeof(IHerbBO))] public class HerbBO : ACommonBO, IHerbBO { public HerbBO() : base() { } [Import] protected override IHerbDAO DbOperator { get; set; } } }
4 总结
本文所描述的基于C#语言的轻量级可扩展应用程序架构,在抽象层积累了大量逻辑与依赖关系,在具象层积累大量互相独立的数据与业务逻辑,用以极力遵循DIP原则,但笔者并无意为某种价值体系摇旗呐喊,仅对长期以来项目经验的粗陋积累,做一次总结和提炼,前路漫漫,所谓路漫漫其修远兮,吾将上下而求索。大道至简,笃笃前行。