C#监控类属性的更改(大花猫动了哪些小玩具)

  实体类创建后在方法中对哪些属性赋值了,传递到底层方法时在底层如何得知哪些属性被赋值过。如何监控属性的更改,请看脑洞大开之《大花猫动了哪些小玩具》——记属性监控之曲线救国。

  在使用EF更新数据库实体时。很多时候我们想要的只是更新表中的某一个或部分字段。虽然可以通过设置来告诉上下文我们要更新的字段。但是一般我们都会把数据持久层封装起来。通过泛型操作。而这时我们就无法得知应用层面修改了哪些字段了。

  最近也在学习EF,就正好遇到了这个问题。当然,如果直接在应用层面使用,通过设置字段的IsModified状态就可以了。如下
  db.Entry(model).Property(x => x.Token).IsModified = false;
  可是,这仅限于学习和demo。正式开发中一般是不会把这种底层操作公开给应用层面的。都会把数据库持久层进行封装。然后通过实体工厂(仓库)加实体泛型的方式提供增删改查。
  具体的可以参考《基于Entity Framework的Repository模式设计》之类的文章。
  这类方式都有一个共同点,更新和删除的时候都有如下类似代码:

  1.     public virtual void Update(TEntity TObject)
  2. {
  3. try
  4. {
  5. var entry = Context.Entry(TObject);
  6. Context.Set<TEntity>().Attach(TObject);
  7. entry.State = EntityState.Modified;
  8. }
  9. catch (OptimisticConcurrencyException ex)
  10. {
  11. throw ex;
  12. }
  13. }

  个人理解:Update(TEntity TObject)通过传递一个实体到方法,然后附加到数据库上下文,并将数据标记为修改状态。然后进行的更新。
  这种情况会对实体的所有字段进行更新。那么我们则需要保证这个实体是从数据库查出来的,或者与数据库的记录是对应的上的。这在C/S结构中是没有问题的,可问题是在B/S结构中呢?我们不可能把实体所有的字段都打包,发送到客户端,然后客户端修改在返回到服务端,然后在调用仓库方法更新吧。说个最简单的,修改用户密码,我们只需要一个用户ID,一个新密码就可以了。或者锁定用户账号,只需要一个用户ID,一个锁定状态,一个锁定时间。这样,我们不可能把整个用户实体打包传来传去吧。有人说可以在保存的时候先根据ID查一遍数据库,然后再将修改的属性值附加上去后再更新就可以了。这就回到问题上了:在仓库方法中只有泛型类型,而你在调用仓库更新方法时传递的是一个实体类型。仓库并不知道你是那个实体,并且更新了哪些字段。
当然,通过触发器我们知道数据库的更新都是先删后插,所以更新几个字段与全列更新底层操作是没有多少区别的。

  现在抛开仓库更新等实体泛型等信息。就单看一下当一个实体发生改变时,我们怎么能知道他修改了哪些属性。
  正常情况下一个实体长这样

  1. /// <summary>
  2. /// 一个具体的实体
  3. /// </summary>
  4. public class AccountEntity : MainEntity
  5. {
  6. /// <summary>
  7. /// 文本类型
  8. /// </summary>
  9. public virtual string Account { get; set; }
  10. /// <summary>
  11. /// 又一个文本属性
  12. /// </summary>
  13. public virtual string Password { get; set; }
  14. /// <summary>
  15. /// 数字类型
  16. /// </summary>
  17. public virtual int Sex { get; set; }
  18. /// <summary>
  19. /// 事件类型
  20. /// </summary>
  21. public virtual DateTime Birthday { get; set; }
  22. /// <summary>
  23. /// 双精度浮点数
  24. /// </summary>
  25. public virtual double Height { get; set; }
  26. /// <summary>
  27. /// 十进制数
  28. /// </summary>
  29. public virtual decimal Monery { get; set; }
  30. /// <summary>
  31. /// 二进制
  32. /// </summary>
  33. public virtual byte[] PublicKey { get; set; }
  34. /// <summary>
  35. /// Guid类型
  36. /// </summary>
  37. public virtual Guid AreaId { get; set; }
  38. }

  当我们要修改这个实体的属性时:

  1. var entity = new accountEntity();
  2. entity.Id=1;
  3. entity.Account = "给属性赋值';

  然后将这个实体传递到底层进行操作。

  1. db.Update(entity);

  完全没有问题,可是我的问题在底层怎么知道我应用层修改了那几个属性呢?再加一个方法,告诉底层,我修改了这几个属性。

  1. db.Update(entity,"Account");

  好像也没有什么不可哈。

  可是这样,如果我修改了Account,参数中却传递了Password怎么办?所以,应该在实体上就应该有一个集合对整个属性是否有修改的状态进行存储。然后到底层Update方法在取出更新过的字段进行下一步操作。
  通过这一思路,我想到在实体中加一个字典:

  1. protected Dictionary<string, dynamic> FieldTracking = new Dictionary<string, dynamic>();

  当属性赋值时,则添加到字典中来。(当然,这种操作是会增加程序的开销的)

  1. FieldTracking["Account"]="给属性赋值";

  然后在底层在取出里面的集合,来区分哪些字段被修改(大花猫动了哪些小玩具)。

  改造下实体属性

  1. public virtual string Account
  2. {
  3. get
  4. { return _Account; }
  5. set {
  6. _Account = value;
  7. FieldTracking["Account"] = value;
  8. }
  9. }

  看过编译后的IL代码的都知道,class中的属性最终会编译成两个方法 setvalue和getvalue,那么通过修改set方法添加FieldTracking["Account"] = value;就可以让属性在赋值的时候添加到字典中。

  很简单吧。

  你以为这样就完了。如果拿房间来比喻实体、拿玩具来比作属性。我家那大花猫就是修改实体属性的方法。你知道我家有多少玩具吗?你每天回家的时候你知道大花猫动了哪个小玩具吗?给每个玩具装个GPS?哈哈哈哈,别闹,花这心思还不如再买点回来。什么?买回来的还得装,算了。研究下怎么装吧。

  一个程序可能有上百个实体类,修改现有的实体类,给每个set加一行?作为一个程序员是不可能容忍做这样的操作的。写一个工具,读取所有的实体代码,加上这一行,保存。这是个好办法。那每次添加一个实体类就得调用工具重写来一遍,每次修改属性再调用一遍,恩。没问题。能用就行。这不是一个真心养猫的人的人能容忍的。

  那怎么办?把猫打死?那玩具的存在将会没有任何意义。想到一个办法,在我离开房子的时候(程序初始化),给房子里的所有房间(实体类)创建一个同样的房间(继承),包含了与原房间所有需要监控(标记为virtual)的玩具的复制,在复制过程中加上GPS(-_~)。然后给猫玩。猫通过我给的门进到这个继承的房间中玩所有玩具的时候,GPS就能将猫的动作全部记录下来。我一回家,这猫玩了哪些玩具一看GPS记录就全知道了。哟,这小崽子,在王元鹅呢。
  

  看不懂,没关系,上马:
  1、在程序集初始化的时候,通过反射,查找所有继承自BaseEntity的实体类。遍历其中的属性。找到标记为virtual进行复制。

    刚开始对于如果找到virtual属性花了不少时间。我总只想着在属性上找,却没想到去set_value方法上去找(其实get_value方法也是)。还是太菜啊。

    注:NoMapAttribute特性是一个自定义的标记,表示不参与映射。因为不参与映射就不需要监控。与本文章代码没有太大的关系。仅供参考。

  1. //获取实体所在的程序集(ClassLibraryDemo)
  2. var assemblyArray = AppDomain.CurrentDomain.GetAssemblies()
  3. .Where(w => w.GetName().Name == "ClassLibraryDemo")
  4. .ToList();
  5. //实体的基类
  6. var baseEntityType = typeof(BaseEntity);
  7. //循环程序集
  8. foreach (Assembly item in assemblyArray)
  9. {
  10. //找到这个程序集中继承自基类的实体
  11. var types = item.GetTypes().Where(t => t.IsAbstract == false
  12. && baseEntityType.IsAssignableFrom(t)
  13. && t != baseEntityType);
  14. foreach (Type btItem in types){
  15. //遍历这个实体类中的属性
  16. var properties = btItem.GetProperties(BindingFlags.Public | BindingFlags.Instance)
  17. .Where(w => w.CanRead && w.CanWrite
  18. && w.GetCustomAttributes(typeof(NoMapAttribute), false).Any() == false
  19. //TODO:要不要检查get方法?
  20. && w.GetSetMethod().IsVirtual);
  21. }
  22. }

  2、根据1的结果,复制一个新的房间(动态代码生成一个类,这个类继承1中的实体,并且重写了属性的set方法)

  这个过程就设计到动态代码的生成了。

  1. //首先创建一个与实体类对应的动态类
  2. CodeTypeDeclaration ct = new CodeTypeDeclaration(btItem.Name + "_Dynamic");
  3. //循环实体中的所有标记为virtual的属性
  4. foreach (PropertyInfo fiItem in properties)
  5. {
  6. //创建一个属性
  7. var p = new CodeMemberProperty();
  8. //设置属性为公共、重写
  9. p.Attributes = MemberAttributes.Public | MemberAttributes.Override;//override
  10. //设置属性的类型为继承的属性的数据类型
  11. p.Type = new CodeTypeReference(fiItem.PropertyType);
  12. //属性名称与继承的一致
  13. p.Name = fiItem.Name;
  14. //包含set代码
  15. p.HasSet = true;
  16. //包含get代码
  17. p.HasGet = true;
  18. //设置get代码
  19. //return base.Account
  20. p.GetStatements.Add(new CodeMethodReturnStatement(
  21. new CodeFieldReferenceExpression(
  22. new CodeBaseReferenceExpression(), fiItem.Name)));
  23. //设置set代码
  24. //base.Account=value;
  25. p.SetStatements.Add(
  26. new CodeAssignStatement(
  27. new CodeFieldReferenceExpression(
  28. new CodeBaseReferenceExpression(), fiItem.Name),
  29. new CodePropertySetValueReferenceExpression()));
  30. //FieldTracking["Account"]=value;
  31. p.SetStatements.Add(new CodeSnippetExpression("FieldTracking[\"" + fiItem.Name + "\"] = value"));
  32. //将属性添加到类中
  33. ct.Members.Add(p);
  34. }

  3、将刚才生成的类加到原类所在的命名空间+".Dynamic"(加后缀以示区分)

  1. //声明一个命名空间(与当前实体类同名+后缀)
  2. CodeNamespace ns = new CodeNamespace(btItem.Namespace + ".Dynamic");
  3. ns.Types.Add(ct);

  4、编辑生成代码所在的程序集

  1. //要动态生成代码的程序集
  2. CodeCompileUnit program = new CodeCompileUnit();
  3. //添加引用
  4. program.ReferencedAssemblies.Add("mscorlib.dll");
  5. program.ReferencedAssemblies.Add("System.dll");
  6. program.ReferencedAssemblies.Add("System.Core.dll");
  7.  
  8. //定义代码工厂
  9. CSharpCodeProvider provider = new CSharpCodeProvider();
  10. //编译程序集
  11. var cr = provider.CompileAssemblyFromDom(new System.CodeDom.Compiler.CompilerParameters();
  12. //看编译是否通过
  13. var error = cr.Errors;
  14. if (error.HasErrors)
  15. {
  16. Console.WriteLine("错误列表:");
  17. //编译不通过
  18. foreach (dynamic item in error)
  19. {
  20. Console.WriteLine("ErrorNumber:{0};Line:{1};ErrorText{2}",
  21. item.ErrorNumber,
  22. item.Line,
  23. item.ErrorText);
  24. }
  25. return;
  26. }
  27. else
  28. {
  29. Console.WriteLine("编译成功。");
  30. }

  查看生成的代码

  1. //查看生成的代码
  2. var codeText = new StringBuilder();
  3. using (var codeWriter = new StringWriter(codeText))
  4. {
  5. CodeDomProvider.CreateProvider("CSharp").GenerateCodeFromNamespace(ns,
  6. codeWriter,
  7. new CodeGeneratorOptions()
  8. {
  9. BlankLinesBetweenMembers = true
  10. });
  11. }
  12. Console.WriteLine(codeText);

  5、将复制的新类与原类建立映射关系。

  1. foreach (Type item in ts)
  2. {
  3. //注册(模拟实现,通过字典实现的,也可以通过IOC注入方式处理)
  4. Mapping.Map(item.BaseType, item);
  5. }

  6、获得这个复制的实体对象

  1. //创建一个指定的实体对象
  2. AccountEntity ae = Mapping.GetMap<AccountEntity>();

  7、对这个实体对象的属性进行赋值

  1. //主键赋值不会修改属性更新
  2. ae.BaseEntity_Id = 1;//不会变(未标记为virtual)
  3. ae.MainEntity_Name = "大花猫";
  4. ae.MainEntity_UpdateTime = DateTime.Now;
  5. //修改某个属性
  6. ae.Account = "admin";
  7. ae.Account = "以最后一次的修改为准";

  8、调用底层方法,底层根据这个实体属性获得被修改的属性名称

  1. //调用基类中的方法 获取变动的属性
  2. var up = ae.GetFieldTracking();
  3. Console.WriteLine("有修改的字段:");
  4. up.ForEach(fe =>
  5. {
  6. Console.WriteLine(fe + ":" + ae[fe]);
  7. });

  9、完美

  

  就这样,在底层就能知道哪些实体被赋值过了。

  当然,有些实体我们只是需要用来计算,则可以调用方法将赋值过的属性进行删除

  1. //删除变更字段
  2. ae.RemoveChanges("Account");

  这只是一个简单的实现,还有一种比较复杂的情况,在第6步,获得这个复制的实体对象时,怎么用一个现有的new出来的实体对象去创建建并监控呢。就像,别人送我一房间现成的玩具,给我的时候猫就在里面玩了。嗷,把猫打死吧。

  总结:

再次认识到反射的强大。
也第一次实现了代码生成代码并使用的经历。
对字段和属性的区别有了更深的认识。
对访问修饰符和虚virtual方法有了更好的认识。

  本文仅供参考,如果你能通过阅读本文解决你的问题或能学到点什么那就更好了。

  源代码被猫吃了,被猫吃了……

C#监控类属性的更改(大花猫动了哪些小玩具)的更多相关文章

  1. Python之路-面向对象&继承和多态&类属性和实例属性&类方法和静态方法

    一.面向对象 编程方式 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发“更快更好更强…” 什么是面 ...

  2. Python:类属性,实例属性,私有属性与静态方法,类方法,实例方法

    From: http://www.cnblogs.com/pengsixiong/p/4823473.html 属性分为实例属性与类属性 方法分为普通方法,类方法,静态方法 一:属性: 尽量把需要用户 ...

  3. 【17】有关python面向对象编程的提高【多继承、多态、类属性、动态添加与限制添加属性与方法、@property】

    一.多继承 案例1:小孩继承自爸爸,妈妈.在程序入口模块再创建实例调用执行 #father模块 class Father(object): def __init__(self,money): self ...

  4. Mybatis框架学习总结-解决字段名与实体类属性名不相同的冲突

    在平时的开发中,我们表中的字段名和表对应实体类的属性名称不一定是完全相同的. 1.准备演示需要使用的表和数据 CREATE TABLE orders( order_id INT PRIMARY KEY ...

  5. iOS8.1 编译ffmpeg和集成第三方实现直播(监控类)

    iOS8.1 编译ffmpeg和集成第三方实现直播(监控类) http://www.mamicode.com/info-detail-476094.html 一,下载并在终端中运行脚本编译ffmpeg ...

  6. Python3 之 类属性与实例属性

    1.类属性与实例属性 类属性就相当与全局变量,实例对象共有的属性,实例对象的属性为实例对象自己私有. 类属性就是类对象(Tool)所拥有的属性,它被所有类对象的实例对象(实例方法)所共有,在内存中只存 ...

  7. python中的实例属性和类属性

    在python中,类属性和实例属性的区别是什么? 我认为是作用域的不同,实例对象可以访问类属性,类对象不可以访问实例属性.(类的概念本身就是作用域的概念,你不能让一只猫会飞,猫属于猫类,这一类都不会飞 ...

  8. Python 进阶_OOP 面向对象编程_类属性和方法

    目录 目录 类属性 调用类属性 查看类属性 特殊的类属性 类方法 真构造器 __new__ 类属性 在理解类属性之前要先搞清楚 实例属性 和 函数属性 之间的区别: 1. 实例属性:指的是实例化类对象 ...

  9. MyBatis 实体类属性与表字段不一致

    原文链接:https://blog.csdn.net/zx48822821/java/article/details/79050735 因为数据库一般设置为表的字段不区分大小写,所以数据库中表的字段通 ...

随机推荐

  1. maven插件打包可执行jar

    pom文件加 <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</grou ...

  2. placeholder属性兼容ie8

    <!doctype html> <html> <head> <meta charset="utf-8" /> <title&g ...

  3. scrapy配置

    scrapy配置 增加并发 并发是指同时处理的request的数量.其有全局限制和局部(每个网站)的限制. Scrapy默认的全局并发限制对同时爬取大量网站的情况并不适用,因此您需要增加这个值. 增加 ...

  4. maven 的docker插件

    首先你得配置一个带有认证的docker私有仓库. 本机要安装maven和jdk vi pom.xml <plugin> <groupId>com.spotify</gro ...

  5. crontab问题处理

    用pyhton写了一些爬虫,由于数据量比较大,需要跑的时间也比较长,所以将代码部署到服务器上.选择用crontab完成爬虫的定时爬取数据,这样避免了人工的干预,减少一些人为错误.但在部署crontab ...

  6. 《基于Node.js实现简易聊天室系列之详细设计》

    一个完整的项目基本分为三个部分:前端.后台和数据库.依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析.概要设计.详细设计.编码.测试等.由于缺乏相关知识的储备,导致这个Demo系列的文章层次 ...

  7. 源码安装zabbix_server服务端

    按照上一篇安装lnmp环境:http://www.cnblogs.com/armo/p/6067716.html 保证lnmp正常运行,然后安装zabbix_server 安装依赖 yum -y in ...

  8. 使用 XML 配置 MyBatis

    构建 SqlSessionFactory 最常见的方式是基于 XML 配置(的构造方式).下面的 mybatis-config.xml 展示了一个 典型的 MyBatis 配置文件的样子: XML C ...

  9. 开源框架GreenDao的操作

    1.为什么需要GreenDao?Google原生API不方便 @1手动组拼SQL语句 @2需要自己写操作数据库代码 @3不能把数据库中的数据映射成对象 @4没有实现关联查询 2.GreenDao是什么 ...

  10. Bootstrap警告框

    前面的话 在网站中,网页总是需要和用户一起做沟通与交流.特别是当用户操作上下文为用户提供一些有效的警示框,比如说告诉用户操作成功.操作错误.提示或者警告等.在Bootstrap框架有一个独立的组件,实 ...