轻量级 C# 可复用系统架构的研究与总结 (MVX+MEF+EF6)


0 前言

  在企业级应用开发周期中,普遍存在随着应用程序规模扩大、版本更迭、甚至人员不稳定等带来整个应用程序系统代码结构混乱不堪、重复代码爆炸、超级函数量陡升等现象,最终影响项目按时交付和验收。笔者在经历过许多这样的开发历程过后,简单的整理形成本文。

1 概述

  本应用程序基础架构是通用型数据处理应用程序(系统)代码架构,采用轻量级架构,适用于团队项目的开展和版本迭代。在整套架构结构中,广泛采用IOC结构,采用精简和可重用的模板代码替代反复重叠且容易出错的工作。

2 结构说明

2.1 应用程序包(子项目)结构

  如上图所示,项目分为

  1. UI(界面)
  2. DataEntity(数据实体)
  3. IDataBiz(数据操作业务接口)
  4. IDataOperate(数据操作接口)
  5. DataBiz(数据业务实现)
  6. DataOperate(数据操作实现)
  7. 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),这样的设计有助于优化数据查询速度。本文采用的示例仅支持上述类型,读者可根据自身实际情况采纳本文思想,而修改其具体实现方案。

注意:实体类存在一对一、一对多、多对多等关系,但本文中实体类仅描述原始的数据库表结构,实体类之间不存在依赖关系。数据关联通过程序逻辑判断。(目的在于降低耦合,避免循环依赖)。

实体模块用于存放各数据库表映射的实体类,实体模块中为各实体类提供了统一管理接口。命名为IEntity。

本示例模拟为中医院药材提供进销存管理功能,项目名为OpenTCM,实体模块各类关系如下:

IOpenTCMEntity接口仅用于规范实体类,并对实体类进行统一标记。该接口必须遵循如下约定:

  1. 必须为泛型接口。
  2. 泛型类型必须为其自身。
  3. 泛型类型通过where约束为class和new(),指定实体类必须是类类型,并具有无参的个构造函数。
  4. 该接口可以不声明任何函数或属性。

这样可以约束实体类,并通过统一实现某接口(如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 DbSet HerbDataSet { 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的实现类,即实体类;IDAO是IBaseDAO,即针对某实体的数据库操作接口。并具有一个Protected修饰的IDAO类型的DbOperator属性,用于子类实现为具体的实体(表)访问接口。

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原则,但笔者并无意为某种价值体系摇旗呐喊,仅对长期以来项目经验的粗陋积累,做一次总结和提炼,前路漫漫,所谓路漫漫其修远兮,吾将上下而求索。大道至简,笃笃前行。